Merge pull request #30 from Mbucari/master

Complete overhaul of download and decrypt.
This commit is contained in:
rmcrackan 2021-06-29 09:35:19 -04:00 committed by GitHub
commit 491a5eba3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1364 additions and 1368 deletions

View File

@ -1,40 +0,0 @@
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Dinah.Core.Diagnostics;
namespace AaxDecrypter
{
public class AAXChapters : Chapters
{
public AAXChapters(string file)
{
var info = new ProcessStartInfo
{
FileName = DecryptSupportLibraries.ffprobePath,
Arguments = "-loglevel panic -show_chapters -print_format xml \"" + file + "\""
};
var xml = info.RunHidden().Output;
var xmlDocument = new System.Xml.XmlDocument();
xmlDocument.LoadXml(xml);
var chaptersXml = xmlDocument.SelectNodes("/ffprobe/chapters/chapter")
.Cast<System.Xml.XmlNode>()
.Where(n => n.Name == "chapter");
foreach (var cnode in chaptersXml)
{
double startTime = double.Parse(cnode.Attributes["start_time"].Value.Replace(",", "."), CultureInfo.InvariantCulture);
double endTime = double.Parse(cnode.Attributes["end_time"].Value.Replace(",", "."), CultureInfo.InvariantCulture);
string chapterTitle = cnode.ChildNodes
.Cast<System.Xml.XmlNode>()
.Where(childnode => childnode.Attributes["key"].Value == "title")
.Select(childnode => childnode.Attributes["value"].Value)
.FirstOrDefault();
AddChapter(new Chapter(startTime, endTime, chapterTitle));
}
}
}
}

View File

@ -15,9 +15,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="DecryptLib\AtomicParsley.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\avcodec-58.dll"> <None Update="DecryptLib\avcodec-58.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
@ -39,9 +36,6 @@
<None Update="DecryptLib\ffprobe.exe"> <None Update="DecryptLib\ffprobe.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="DecryptLib\postproc-54.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\swresample-3.dll"> <None Update="DecryptLib\swresample-3.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>

View File

@ -1,419 +0,0 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Dinah.Core;
using Dinah.Core.Diagnostics;
using Dinah.Core.IO;
using Dinah.Core.StepRunner;
namespace AaxDecrypter
{
public interface ISimpleAaxToM4bConverter
{
event EventHandler<int> DecryptProgressUpdate;
bool Run();
string AppName { get; set; }
string inputFileName { get; }
byte[] coverBytes { get; }
string outDir { get; }
string outputFileName { get; }
Chapters chapters { get; }
Tags tags { get; }
EncodingInfo encodingInfo { get; }
void SetOutputFilename(string outFileName);
}
public interface IAdvancedAaxToM4bConverter : ISimpleAaxToM4bConverter
{
bool Step1_CreateDir();
bool Step2_DecryptAax();
bool Step3_Chapterize();
bool Step4_InsertCoverArt();
bool Step5_Cleanup();
bool Step6_AddTags();
bool End_CreateCue();
bool End_CreateNfo();
}
/// <summary>full c# app. integrated logging. no UI</summary>
public class AaxToM4bConverter : IAdvancedAaxToM4bConverter
{
public event EventHandler<int> DecryptProgressUpdate;
public string inputFileName { get; }
public string audible_key { get; private set; }
public string audible_iv { get; private set; }
private StepSequence steps { get; }
public byte[] coverBytes { get; private set; }
public string AppName { get; set; } = nameof(AaxToM4bConverter);
public string outDir { get; private set; }
public string outputFileName { get; private set; }
public Chapters chapters { get; private set; }
public Tags tags { get; private set; }
public EncodingInfo encodingInfo { get; private set; }
public static async Task<AaxToM4bConverter> CreateAsync(string inputFile, string audible_key, string audible_iv, Chapters chapters = null)
{
var converter = new AaxToM4bConverter(inputFile, audible_key, audible_iv);
converter.chapters = chapters ?? new AAXChapters(inputFile);
await converter.prelimProcessing();
converter.printPrelim();
return converter;
}
private AaxToM4bConverter(string inputFile, string audible_key, string audible_iv)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(inputFile, nameof(inputFile));
ArgumentValidator.EnsureNotNullOrWhiteSpace(audible_key, nameof(audible_key));
ArgumentValidator.EnsureNotNullOrWhiteSpace(audible_iv, nameof(audible_iv));
if (!File.Exists(inputFile))
throw new ArgumentNullException(nameof(inputFile), "File does not exist");
steps = new StepSequence
{
Name = "Convert Aax To M4b",
["Step 1: Create Dir"] = Step1_CreateDir,
["Step 2: Decrypt Aax"] = Step2_DecryptAax,
["Step 3: Chapterize and tag"] = Step3_Chapterize,
["Step 4: Insert Cover Art"] = Step4_InsertCoverArt,
["Step 5: Cleanup"] = Step5_Cleanup,
["Step 6: Add Tags"] = Step6_AddTags,
["End: Create Cue"] = End_CreateCue,
["End: Create Nfo"] = End_CreateNfo
};
inputFileName = inputFile;
this.audible_key = audible_key;
this.audible_iv = audible_iv;
}
private async Task prelimProcessing()
{
tags = new Tags(inputFileName);
encodingInfo = new EncodingInfo(inputFileName);
var defaultFilename = Path.Combine(
Path.GetDirectoryName(inputFileName),
PathLib.ToPathSafeString(tags.author),
PathLib.ToPathSafeString(tags.title) + ".m4b"
);
// set default name
SetOutputFilename(defaultFilename);
await Task.Run(() => saveCover(inputFileName));
}
private void saveCover(string aaxFile)
{
using var file = TagLib.File.Create(aaxFile, "audio/mp4", TagLib.ReadStyle.Average);
coverBytes = file.Tag.Pictures[0].Data.Data;
}
private void printPrelim()
{
Console.WriteLine($"Audible Book ID = {tags.id}");
Console.WriteLine($"Book: {tags.title}");
Console.WriteLine($"Author: {tags.author}");
Console.WriteLine($"Narrator: {tags.narrator}");
Console.WriteLine($"Year: {tags.year}");
Console.WriteLine($"Total Time: {tags.duration.GetTotalTimeFormatted()} in {chapters.Count} chapters");
Console.WriteLine($"WARNING-Source is {encodingInfo.originalBitrate} kbits @ {encodingInfo.sampleRate}Hz, {encodingInfo.channels} channels");
}
public bool Run()
{
var (IsSuccess, Elapsed) = steps.Run();
if (!IsSuccess)
{
Console.WriteLine("WARNING-Conversion failed");
return false;
}
var speedup = (int)(tags.duration.TotalSeconds / (long)Elapsed.TotalSeconds);
Console.WriteLine("Speedup is " + speedup + "x realtime.");
Console.WriteLine("Done");
return true;
}
public void SetOutputFilename(string outFileName)
{
outputFileName = PathLib.ReplaceExtension(outFileName, ".m4b");
outDir = Path.GetDirectoryName(outputFileName);
if (File.Exists(outputFileName))
File.Delete(outputFileName);
}
private string outputFileWithNewExt(string extension) => PathLib.ReplaceExtension(outputFileName, extension);
public bool Step1_CreateDir()
{
ProcessRunner.WorkingDir = outDir;
Directory.CreateDirectory(outDir);
return true;
}
public bool Step2_DecryptAax()
{
DecryptProgressUpdate?.Invoke(this, 0);
var tempRipFile = Path.Combine(outDir, "funny.aac");
var fail = "WARNING-Decrypt failure. ";
int returnCode;
returnCode = decrypt(tempRipFile);
if (returnCode == -99)
Console.WriteLine($"{fail}Incorrect decrypt key.");
else if (returnCode == 100)
Console.WriteLine($"{fail}Thread completed without changing return code. This shouldn't be possible");
else if (returnCode == 0)
{
// success!
FileExt.SafeMove(tempRipFile, outputFileWithNewExt(".mp4"));
DecryptProgressUpdate?.Invoke(this, 100);
return true;
}
else // any other returnCode
Console.WriteLine($"{fail}Unknown failure code: {returnCode}");
FileExt.SafeDelete(tempRipFile);
DecryptProgressUpdate?.Invoke(this, 0);
return false;
}
private int decrypt(string tempRipFile)
{
FileExt.SafeDelete(tempRipFile);
Console.WriteLine($"Decrypting with key={audible_key}, iv={audible_iv}");
var returnCode = 100;
var thread = new Thread(_ => returnCode = ngDecrypt(tempRipFile));
thread.Start();
double fileLen = new FileInfo(inputFileName).Length;
while (thread.IsAlive && returnCode == 100)
{
Thread.Sleep(500);
if (File.Exists(tempRipFile))
{
double tempLen = new FileInfo(tempRipFile).Length;
var percentProgress = tempLen / fileLen * 100.0;
DecryptProgressUpdate?.Invoke(this, (int)percentProgress);
}
}
return returnCode;
}
private int ngDecrypt(string tempFileName)
{
#region avformat-58.dll HACK EXPLANATION
/* avformat-58.dll HACK EXPLANATION
*
* FFMPEG refused to copy the aac stream from AAXC files with 44kHz sample rates
* with error "Scalable configurations are not allowed in ADTS". The adts encoder
* can be found on github at:
* https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/adtsenc.c
*
* adtsenc detects scalable aac by a flag in the aac metadata and throws an error if
* it is set. It appears that all aaxc files contain aac streams that can be written
* to adts, but either the codec is parsing the header incorrectly or the aaxc
* header is incorrect.
*
* As a workaround, i've modified avformat-58.dll to allow adtsenc to ignore the
* scalable flag and continue. To modify:
*
* Open ffmpeg.exe in x64dbg (https://x64dbg.com)
*
* Navigate to the avformat module and search for the error string "Scalable
* configurations are not allowed in ADTS". (00007FFE16AA5899 in example below).
*
* 00007FFE16AA587B | 4C:8D05 DE5E6900 | lea r8,qword ptr ds:[7FFE1713B760] | 00007FFE1713B760:"960/120 MDCT window is not allowed in ADTS\n"
* 00007FFE16AA5882 | BA 10000000 | mov edx,10 |
* 00007FFE16AA5887 | 4C:89F1 | mov rcx,r14 |
* 00007FFE16AA588A | E8 697A1900 | call <JMP.&av_log> |
* 00007FFE16AA588F | B8 B7B1BBBE | mov eax,BEBBB1B7 |
* 00007FFE16AA5894 | E9 D5F8FFFF | jmp avformat-58.7FFE16AA516E |
* 00007FFE16AA5899 | 4C:8D05 F05E6900 | lea r8,qword ptr ds:[7FFE1713B790] | 00007FFE1713B790:"Scalable configurations are not allowed in ADTS\n"
* 00007FFE16AA58A0 | BA 10000000 | mov edx,10 |
* 00007FFE16AA58A5 | 4C:89F1 | mov rcx,r14 |
* 00007FFE16AA58A8 | E8 4B7A1900 | call <JMP.&av_log> |
* 00007FFE16AA58AD | B8 B7B1BBBE | mov eax,BEBBB1B7 |
* 00007FFE16AA58B2 | E9 B7F8FFFF | jmp avformat-58.7FFE16AA516E |
* 00007FFE16AA58B7 | 4C:8D05 4A5E6900 | lea r8,qword ptr ds:[7FFE1713B708] | 00007FFE1713B708:"MPEG-4 AOT %d is not allowed in ADTS\n"
* 00007FFE16AA58BE | BA 10000000 | mov edx,10 |
* 00007FFE16AA58C3 | 4C:89F1 | mov rcx,r14 |
* 00007FFE16AA58C6 | E8 2D7A1900 | call <JMP.&av_log> |
* 00007FFE16AA58CB | B8 B7B1BBBE | mov eax,BEBBB1B7 |
* 00007FFE16AA58D0 | E9 99F8FFFF | jmp avformat-58.7FFE16AA516E |
* 00007FFE16AA58D5 | 4C:8D05 EC5E6900 | lea r8,qword ptr ds:[7FFE1713B7C8] | 00007FFE1713B7C8:"Extension flag is not allowed in ADTS\n"
* 00007FFE16AA58DC | BA 10000000 | mov edx,10 |
* 00007FFE16AA58E1 | 4C:89F1 | mov rcx,r14 |
* 00007FFE16AA58E4 | E8 0F7A1900 | call <JMP.&av_log> |
* 00007FFE16AA58E9 | B8 B7B1BBBE | mov eax,BEBBB1B7 |
* 00007FFE16AA58EE | E9 7BF8FFFF | jmp avformat-58.7FFE16AA516E |
* 00007FFE16AA58F3 | 4C:8D05 365E6900 | lea r8,qword ptr ds:[7FFE1713B730] | 00007FFE1713B730:"Escape sample rate index illegal in ADTS\n"
* 00007FFE16AA58FA | BA 10000000 | mov edx,10 |
* 00007FFE16AA58FF | 4C:89F1 | mov rcx,r14 |
* 00007FFE16AA5902 | E8 F1791900 | call <JMP.&av_log> |
* 00007FFE16AA5907 | B8 B7B1BBBE | mov eax,BEBBB1B7 |
* 00007FFE16AA590C | E9 5DF8FFFF | jmp avformat-58.7FFE16AA516E |
*
* Select the instruction that loads the error string's address, and search for all
* references. You should only find one referance, a conditional jump
* (00007FFE16AA513C example below).
*
* 00007FFE16AA511D | 89C2 | mov edx,eax |
* 00007FFE16AA511F | 89C1 | mov ecx,eax |
* 00007FFE16AA5121 | 83C0 01 | add eax,1 |
* 00007FFE16AA5124 | C1EA 03 | shr edx,3 |
* 00007FFE16AA5127 | 83E1 07 | and ecx,7 |
* 00007FFE16AA512A | 41:8B1414 | mov edx,dword ptr ds:[r12+rdx] |
* 00007FFE16AA512E | 0FCA | bswap edx |
* 00007FFE16AA5130 | D3E2 | shl edx,cl |
* 00007FFE16AA5132 | C1EA FF | shr edx,FF |
* 00007FFE16AA5135 | 39F8 | cmp eax,edi |
* 00007FFE16AA5137 | 0F47C7 | cmova eax,edi |
* 00007FFE16AA513A | 85D2 | test edx,edx |
* 00007FFE16AA513C | 0F85 57070000 | jne avformat-58.7FFE16AA5899 |
*
* Edit that jump with six nop instructions and save the patched assembly.
*/
#endregion
string args = "-audible_key "
+ audible_key
+ " -audible_iv "
+ audible_iv
+ " -i "
+ "\"" + inputFileName + "\""
+ " -c:a copy -vn -sn -dn -y "
+ "\"" + tempFileName + "\"";
var info = new ProcessStartInfo
{
FileName = DecryptSupportLibraries.ffmpegPath,
Arguments = args
};
var result = info.RunHidden();
// failed to decrypt
if (result.Error.Contains("aac bitstream error"))
return -99;
return result.ExitCode;
}
// temp file names for steps 3, 4, 5
string tempChapsGuid { get; } = Guid.NewGuid().ToString().ToUpper().Replace("-", "");
string tempChapsPath => Path.Combine(outDir, $"tempChaps_{tempChapsGuid}.mp4");
string mp4_file => outputFileWithNewExt(".mp4");
string ff_txt_file => mp4_file + ".ff.txt";
public bool Step3_Chapterize()
{
var str1 = "";
if (chapters.FirstChapter.StartTime != 0.0)
{
str1 = " -ss " + chapters.FirstChapter.StartTime.ToString("0.000", CultureInfo.InvariantCulture) + " -t " + chapters.LastChapter.EndTime.ToString("0.000", CultureInfo.InvariantCulture) + " ";
}
var ffmpegTags = tags.GenerateFfmpegTags();
var ffmpegChapters = chapters.GenerateFfmpegChapters();
File.WriteAllText(ff_txt_file, ffmpegTags + ffmpegChapters);
var tagAndChapterInfo = new ProcessStartInfo
{
FileName = DecryptSupportLibraries.ffmpegPath,
Arguments = "-y -i \"" + mp4_file + "\" -f ffmetadata -i \"" + ff_txt_file + "\" -map_metadata 1 -bsf:a aac_adtstoasc -c:a copy" + str1 + " -map 0 \"" + tempChapsPath + "\""
};
var thread = new Thread(_ => tagAndChapterInfo.RunHidden());
thread.Start();
double fileLen = new FileInfo(mp4_file).Length;
while (thread.IsAlive)
{
Thread.Sleep(500);
if (File.Exists(tempChapsPath))
{
double tempLen = new FileInfo(tempChapsPath).Length;
var percentProgress = tempLen / fileLen * 100.0;
DecryptProgressUpdate?.Invoke(this, (int)percentProgress);
}
}
DecryptProgressUpdate?.Invoke(this, 0);
return true;
}
public bool Step4_InsertCoverArt()
{
// save cover image as temp file
var coverPath = Path.Combine(outDir, "cover-" + Guid.NewGuid() + ".jpg");
FileExt.CreateFile(coverPath, coverBytes);
var insertCoverArtInfo = new ProcessStartInfo
{
FileName = DecryptSupportLibraries.atomicParsleyPath,
Arguments = "\"" + tempChapsPath + "\" --encodingTool \"" + AppName + "\" --artwork \"" + coverPath + "\" --overWrite"
};
insertCoverArtInfo.RunHidden();
// delete temp file
FileExt.SafeDelete(coverPath);
return true;
}
public bool Step5_Cleanup()
{
FileExt.SafeDelete(mp4_file);
FileExt.SafeDelete(ff_txt_file);
FileExt.SafeMove(tempChapsPath, outputFileName);
return true;
}
public bool Step6_AddTags()
{
tags.AddAppleTags(outputFileName);
return true;
}
public bool End_CreateCue()
{
File.WriteAllText(outputFileWithNewExt(".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), chapters));
return true;
}
public bool End_CreateNfo()
{
File.WriteAllText(outputFileWithNewExt(".nfo"), NFO.CreateContents(AppName, tags, encodingInfo, chapters));
return true;
}
}
}

View File

@ -0,0 +1,279 @@
using Dinah.Core;
using Dinah.Core.Diagnostics;
using Dinah.Core.IO;
using Dinah.Core.StepRunner;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace AaxDecrypter
{
public interface ISimpleAaxToM4bConverter
{
event EventHandler<int> DecryptProgressUpdate;
bool Run();
string AppName { get; set; }
string outDir { get; }
string outputFileName { get; }
ChapterInfo chapters { get; }
void SetOutputFilename(string outFileName);
string Title { get; }
string Author { get; }
string Narrator { get; }
byte[] CoverArt { get; }
}
public interface IAdvancedAaxcToM4bConverter : ISimpleAaxToM4bConverter
{
void Cancel();
bool Step1_CreateDir();
bool Step2_DownloadAndCombine();
bool Step3_RestoreMetadata();
bool Step4_CreateCue();
bool Step5_CreateNfo();
}
public class AaxcDownloadConverter : IAdvancedAaxcToM4bConverter
{
public event EventHandler<int> DecryptProgressUpdate;
public event EventHandler<TimeSpan> DecryptTimeRemaining;
public string AppName { get; set; } = nameof(AaxcDownloadConverter);
public string outDir { get; private set; }
public string outputFileName { get; private set; }
public ChapterInfo chapters { get; private set; }
public string Title => aaxcTagLib.Tag.Title.Replace(" (Unabridged)", "");
public string Author => aaxcTagLib.Tag.FirstPerformer ?? "[unknown]";
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 DownloadLicense downloadLicense { get; set; }
private FFMpegAaxcProcesser aaxcProcesser;
public static async Task<AaxcDownloadConverter> CreateAsync(string outDirectory, DownloadLicense dlLic, ChapterInfo chapters = null)
{
var converter = new AaxcDownloadConverter(outDirectory, dlLic, chapters);
await converter.prelimProcessing();
return converter;
}
private AaxcDownloadConverter(string outDirectory, DownloadLicense dlLic, ChapterInfo chapters)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(outDirectory, nameof(outDirectory));
ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
if (!Directory.Exists(outDirectory))
throw new ArgumentNullException(nameof(outDirectory), "Directory does not exist");
outDir = outDirectory;
steps = new StepSequence
{
Name = "Convert Aax To M4b",
["Step 1: Create Dir"] = Step1_CreateDir,
["Step 2: Download and Combine Audiobook"] = Step2_DownloadAndCombine,
["Step 3: Restore Aaxc Metadata"] = Step3_RestoreMetadata,
["Step 4: Create Cue"] = Step4_CreateCue,
["Step 5: Create Nfo"] = Step5_CreateNfo,
};
downloadLicense = dlLic;
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)
{
outputFileName = PathLib.ReplaceExtension(outFileName, ".m4b");
outDir = Path.GetDirectoryName(outputFileName);
if (File.Exists(outputFileName))
File.Delete(outputFileName);
}
public bool Run()
{
var (IsSuccess, Elapsed) = steps.Run();
if (!IsSuccess)
{
Console.WriteLine("WARNING-Conversion failed");
return false;
}
var speedup = (int)(aaxcTagLib.Properties.Duration.TotalSeconds / (long)Elapsed.TotalSeconds);
Console.WriteLine("Speedup is " + speedup + "x realtime.");
Console.WriteLine("Done");
return true;
}
public bool Step1_CreateDir()
{
ProcessRunner.WorkingDir = outDir;
Directory.CreateDirectory(outDir);
return true;
}
public bool Step2_DownloadAndCombine()
{
aaxcProcesser = new FFMpegAaxcProcesser(downloadLicense);
aaxcProcesser.ProgressUpdate += AaxcProcesser_ProgressUpdate;
bool userSuppliedChapters = chapters != null;
string metadataPath = null;
if (userSuppliedChapters)
{
//Only write chaopters to the metadata file. All other aaxc metadata will be
//wiped out but is restored in Step 3.
metadataPath = Path.Combine(outDir, Path.GetFileName(outputFileName) + ".ffmeta");
File.WriteAllText(metadataPath, chapters.ToFFMeta(true));
}
aaxcProcesser.ProcessBook(
outputFileName,
metadataPath)
.GetAwaiter()
.GetResult();
if (!userSuppliedChapters && aaxcProcesser.Succeeded)
chapters = new ChapterInfo(outputFileName);
if (userSuppliedChapters)
FileExt.SafeDelete(metadataPath);
DecryptProgressUpdate?.Invoke(this, 0);
return aaxcProcesser.Succeeded;
}
private void AaxcProcesser_ProgressUpdate(object sender, TimeSpan e)
{
double averageRate = getAverageProcessRate(e);
double remainingSecsToProcess = (aaxcTagLib.Properties.Duration - e).TotalSeconds;
double estTimeRemaining = remainingSecsToProcess / averageRate;
if (double.IsNormal(estTimeRemaining))
DecryptTimeRemaining?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining));
double progressPercent = 100 * e.TotalSeconds / aaxcTagLib.Properties.Duration.TotalSeconds;
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>
/// Copy all aacx metadata to m4b file, including cover art.
/// </summary>
public bool Step3_RestoreMetadata()
{
var outFile = new TagLib.Mpeg4.File(outputFileName, TagLib.ReadStyle.Average);
var destTags = outFile.GetTag(TagLib.TagTypes.Apple) as TagLib.Mpeg4.AppleTag;
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.Save();
return true;
}
public bool Step4_CreateCue()
{
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), chapters));
return true;
}
public bool Step5_CreateNfo()
{
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxcTagLib, chapters));
return true;
}
public void Cancel()
{
aaxcProcesser.Cancel();
}
}
}

View File

@ -1,27 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AaxDecrypter
{
public class Chapter
{
public Chapter(double startTime, double endTime, string title)
{
StartTime = startTime;
EndTime = endTime;
Title = title;
}
/// <summary>
/// Chapter start time, in seconds.
/// </summary>
public double StartTime { get; private set; }
/// <summary>
/// Chapter end time, in seconds.
/// </summary>
public double EndTime { get; private set; }
public string Title { get; private set; }
}
}

View File

@ -1,40 +1,91 @@
using System; using Dinah.Core;
using Dinah.Core.Diagnostics;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
namespace AaxDecrypter namespace AaxDecrypter
{ {
public abstract class Chapters public class ChapterInfo
{ {
private List<Chapter> _chapterList = new(); private List<Chapter> _chapterList = new List<Chapter>();
public IEnumerable<Chapter> Chapters => _chapterList.AsEnumerable();
public int Count => _chapterList.Count; public int Count => _chapterList.Count;
public Chapter FirstChapter => _chapterList[0];
public Chapter LastChapter => _chapterList[Count - 1]; public ChapterInfo() { }
public IEnumerable<Chapter> ChapterList => _chapterList.AsEnumerable(); public ChapterInfo(string audiobookFile)
public IEnumerable<TimeSpan> GetBeginningTimes() => ChapterList.Select(c => TimeSpan.FromSeconds(c.StartTime));
protected void AddChapter(Chapter chapter)
{ {
var info = new ProcessStartInfo
{
FileName = DecryptSupportLibraries.ffprobePath,
Arguments = "-loglevel panic -show_chapters -print_format xml \"" + audiobookFile + "\""
};
var xml = info.RunHidden().Output;
var xmlDocument = new System.Xml.XmlDocument();
xmlDocument.LoadXml(xml);
var chaptersXml = xmlDocument.SelectNodes("/ffprobe/chapters/chapter")
.Cast<System.Xml.XmlNode>()
.Where(n => n.Name == "chapter");
foreach (var cnode in chaptersXml)
{
double startTime = double.Parse(cnode.Attributes["start_time"].Value.Replace(",", "."), CultureInfo.InvariantCulture);
double endTime = double.Parse(cnode.Attributes["end_time"].Value.Replace(",", "."), CultureInfo.InvariantCulture);
string chapterTitle = cnode.ChildNodes
.Cast<System.Xml.XmlNode>()
.Where(childnode => childnode.Attributes["key"].Value == "title")
.Select(childnode => childnode.Attributes["value"].Value)
.FirstOrDefault();
AddChapter(new Chapter(chapterTitle, (long)(startTime * 1000), (long)((endTime - startTime) * 1000)));
}
}
public void AddChapter(Chapter chapter)
{
ArgumentValidator.EnsureNotNull(chapter, nameof(chapter));
_chapterList.Add(chapter); _chapterList.Add(chapter);
} }
protected void AddChapters(IEnumerable<Chapter> chapters) public string ToFFMeta(bool includeFFMetaHeader)
{ {
_chapterList.AddRange(chapters); var ffmetaChapters = new StringBuilder();
}
public string GenerateFfmpegChapters()
{
var stringBuilder = new StringBuilder();
foreach (Chapter c in ChapterList) if (includeFFMetaHeader)
ffmetaChapters.AppendLine(";FFMETADATA1\n");
foreach (var c in Chapters)
{ {
stringBuilder.Append("[CHAPTER]\n"); ffmetaChapters.AppendLine(c.ToFFMeta());
stringBuilder.Append("TIMEBASE=1/1000\n"); }
stringBuilder.Append("START=" + c.StartTime * 1000 + "\n"); return ffmetaChapters.ToString();
stringBuilder.Append("END=" + c.EndTime * 1000 + "\n"); }
stringBuilder.Append("title=" + c.Title + "\n"); }
public class Chapter
{
public string Title { get; }
public long StartOffsetMs { get; }
public long EndOffsetMs { get; }
public Chapter(string title, long startOffsetMs, long lengthMs)
{
ArgumentValidator.EnsureNotNullOrEmpty(title, nameof(title));
ArgumentValidator.EnsureGreaterThan(startOffsetMs, nameof(startOffsetMs), -1);
ArgumentValidator.EnsureGreaterThan(lengthMs, nameof(lengthMs), 0);
Title = title;
StartOffsetMs = startOffsetMs;
EndOffsetMs = StartOffsetMs + lengthMs;
} }
return stringBuilder.ToString(); public string ToFFMeta()
{
return "[CHAPTER]\n" +
"TIMEBASE=1/1000\n" +
"START=" + StartOffsetMs + "\n" +
"END=" + EndOffsetMs + "\n" +
"title=" + Title;
} }
} }
} }

View File

@ -1,6 +1,5 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using Dinah.Core; using Dinah.Core;
@ -8,17 +7,17 @@ namespace AaxDecrypter
{ {
public static class Cue public static class Cue
{ {
public static string CreateContents(string filePath, Chapters chapters) public static string CreateContents(string filePath, ChapterInfo chapters)
{ {
var stringBuilder = new StringBuilder(); var stringBuilder = new StringBuilder();
stringBuilder.AppendLine(GetFileLine(filePath, "MP3")); stringBuilder.AppendLine(GetFileLine(filePath, "MP3"));
var trackCount = 0; var trackCount = 0;
foreach (Chapter c in chapters.ChapterList) foreach (var c in chapters.Chapters)
{ {
trackCount++; trackCount++;
var startTime = TimeSpan.FromSeconds(c.StartTime); var startTime = TimeSpan.FromMilliseconds(c.StartOffsetMs);
stringBuilder.AppendLine($"TRACK {trackCount} AUDIO"); stringBuilder.AppendLine($"TRACK {trackCount} AUDIO");
stringBuilder.AppendLine($" TITLE \"{c.Title}\""); stringBuilder.AppendLine($" TITLE \"{c.Title}\"");

View File

@ -6,13 +6,11 @@ namespace AaxDecrypter
{ {
// OTHER EXTERNAL DEPENDENCIES // OTHER EXTERNAL DEPENDENCIES
// ffprobe has these pre-req.s as I'm using it: // ffprobe has these pre-req.s as I'm using it:
// avcodec-58.dll, avdevice-58.dll, avfilter-7.dll, avformat-58.dll, avutil-56.dll, postproc-54.dll, swresample-3.dll, swscale-5.dll, taglib-sharp.dll // avcodec-58.dll, avdevice-58.dll, avfilter-7.dll, avformat-58.dll, avutil-56.dll, swresample-3.dll, swscale-5.dll, taglib-sharp.dll
private static string appPath_ { get; } = Path.GetDirectoryName(Dinah.Core.Exe.FileLocationOnDisk); private static string appPath_ { get; } = Path.GetDirectoryName(Dinah.Core.Exe.FileLocationOnDisk);
private static string decryptLib_ { get; } = Path.Combine(appPath_, "DecryptLib"); private static string decryptLib_ { get; } = Path.Combine(appPath_, "DecryptLib");
public static string ffmpegPath { get; } = Path.Combine(decryptLib_, "ffmpeg.exe"); public static string ffmpegPath { get; } = Path.Combine(decryptLib_, "ffmpeg.exe");
public static string ffprobePath { get; } = Path.Combine(decryptLib_, "ffprobe.exe"); public static string ffprobePath { get; } = Path.Combine(decryptLib_, "ffprobe.exe");
public static string atomicParsleyPath { get; } = Path.Combine(decryptLib_, "AtomicParsley.exe");
} }
} }

View File

@ -0,0 +1,30 @@
using Dinah.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AaxDecrypter
{
public class DownloadLicense
{
public string DownloadUrl { get; }
public string AudibleKey { get; }
public string AudibleIV { get; }
public string UserAgent { get; }
public DownloadLicense(string downloadUrl, string audibleKey, string audibleIV, string userAgent)
{
ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
ArgumentValidator.EnsureNotNullOrEmpty(audibleKey, nameof(audibleKey));
ArgumentValidator.EnsureNotNullOrEmpty(audibleIV, nameof(audibleIV));
ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
DownloadUrl = downloadUrl;
AudibleKey = audibleKey;
AudibleIV = audibleIV;
UserAgent = userAgent;
}
}
}

View File

@ -1,41 +0,0 @@
using System;
using System.Diagnostics;
using Dinah.Core.Diagnostics;
namespace AaxDecrypter
{
public class EncodingInfo
{
public int sampleRate { get; } = 44100;
public int channels { get; } = 2;
public int originalBitrate { get; }
public EncodingInfo(string file)
{
var info = new ProcessStartInfo
{
FileName = DecryptSupportLibraries.ffprobePath,
Arguments = "-loglevel panic -show_streams -print_format flat \"" + file + "\""
};
var end = info.RunHidden().Output;
foreach (string str2 in end.Split('\n'))
{
string[] strArray = str2.Split('=');
switch (strArray[0])
{
case "streams.stream.0.channels":
this.channels = int.Parse(strArray[1].Replace("\"", "").TrimEnd('\r', '\n'));
break;
case "streams.stream.0.sample_rate":
this.sampleRate = int.Parse(strArray[1].Replace("\"", "").TrimEnd('\r', '\n'));
break;
case "streams.stream.0.bit_rate":
string s = strArray[1].Replace("\"", "").TrimEnd('\r', '\n');
this.originalBitrate = (int)Math.Round(double.Parse(s) / 1000.0, MidpointRounding.AwayFromZero);
break;
}
}
}
}
}

View File

@ -0,0 +1,219 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace AaxDecrypter
{
/// <summary>
/// Download audible aaxc, decrypt, and remux with chapters.
/// </summary>
class FFMpegAaxcProcesser
{
public event EventHandler<TimeSpan> ProgressUpdate;
public string FFMpegPath { get; }
public DownloadLicense DownloadLicense { get; }
public bool IsRunning { get; private set; }
public bool Succeeded { get; private set; }
public string FFMpegRemuxerStandardError => remuxerError.ToString();
public string FFMpegDownloaderStandardError => downloaderError.ToString();
private StringBuilder remuxerError = 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 Process downloader;
private Process remuxer;
public FFMpegAaxcProcesser( DownloadLicense downloadLicense)
{
FFMpegPath = DecryptSupportLibraries.ffmpegPath;
DownloadLicense = downloadLicense;
}
public async Task ProcessBook(string outputFile, string ffmetaChaptersPath = null)
{
//This process gets the aaxc from the url and streams the decrypted
//aac stream to standard output
downloader = new Process
{
StartInfo = getDownloaderStartInfo()
};
//This process retreves an aac stream from standard input and muxes
// it into an m4b along with the cover art and metadata.
remuxer = new Process
{
StartInfo = getRemuxerStartInfo(outputFile, ffmetaChaptersPath)
};
IsRunning = true;
downloader.ErrorDataReceived += Downloader_ErrorDataReceived;
downloader.Start();
downloader.BeginErrorReadLine();
remuxer.ErrorDataReceived += Remuxer_ErrorDataReceived;
remuxer.Start();
remuxer.BeginErrorReadLine();
var pipedOutput = downloader.StandardOutput.BaseStream;
var pipedInput = remuxer.StandardInput.BaseStream;
//All the work done here. Copy download standard output into
//remuxer standard input
await Task.Run(() =>
{
int lastRead = 0;
byte[] buffer = new byte[32 * 1024];
do
{
lastRead = pipedOutput.Read(buffer, 0, buffer.Length);
pipedInput.Write(buffer, 0, lastRead);
} while (lastRead > 0 && !remuxer.HasExited);
});
//Closing input stream terminates remuxer
pipedInput.Close();
//If the remuxer exited due to failure, downloader will still have
//data in the pipe. Force kill downloader to continue.
if (remuxer.HasExited && !downloader.HasExited)
downloader.Kill();
remuxer.WaitForExit();
downloader.WaitForExit();
IsRunning = false;
Succeeded = downloader.ExitCode == 0 && remuxer.ExitCode == 0;
}
public void Cancel()
{
if (IsRunning && !remuxer.HasExited)
remuxer.Kill();
}
private void Downloader_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (string.IsNullOrEmpty(e.Data))
return;
downloaderError.AppendLine(e.Data);
}
private void Remuxer_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (string.IsNullOrEmpty(e.Data))
return;
remuxerError.AppendLine(e.Data);
if (processedTimeRegex.IsMatch(e.Data))
{
//get timestamp of of last processed audio stream position
var match = processedTimeRegex.Match(e.Data);
int hours = int.Parse(match.Groups[1].Value);
int minutes = int.Parse(match.Groups[2].Value);
int seconds = int.Parse(match.Groups[3].Value);
var position = new TimeSpan(hours, minutes, seconds);
ProgressUpdate?.Invoke(sender, position);
}
if (e.Data.Contains("aac bitstream error"))
{
//This happens if input is corrupt (should never happen) or if caller
//supplied wrong key/iv
var process = sender as Process;
process.Kill();
}
}
private ProcessStartInfo getDownloaderStartInfo() =>
new ProcessStartInfo
{
FileName = FFMpegPath,
RedirectStandardError = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
ArgumentList ={
"-nostdin",
"-audible_key",
DownloadLicense.AudibleKey,
"-audible_iv",
DownloadLicense.AudibleIV,
"-user_agent",
DownloadLicense.UserAgent, //user-agent is requied for CDN to serve the file
"-i",
DownloadLicense.DownloadUrl,
"-c:a", //audio codec
"copy", //copy stream
"-f", //force output format: adts
"adts",
"pipe:" //pipe output to stdout
}
};
private ProcessStartInfo getRemuxerStartInfo(string outputFile, string ffmetaChaptersPath = null)
{
var startInfo = new ProcessStartInfo
{
FileName = FFMpegPath,
RedirectStandardError = true,
RedirectStandardInput = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
};
startInfo.ArgumentList.Add("-thread_queue_size");
startInfo.ArgumentList.Add("1024");
startInfo.ArgumentList.Add("-f"); //force input format: aac
startInfo.ArgumentList.Add("aac");
startInfo.ArgumentList.Add("-i"); //read input from stdin
startInfo.ArgumentList.Add("pipe:");
if (ffmetaChaptersPath is null)
{
//copy metadata from aaxc file.
startInfo.ArgumentList.Add("-user_agent");
startInfo.ArgumentList.Add(DownloadLicense.UserAgent);
startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(DownloadLicense.DownloadUrl);
}
else
{
//copy metadata from supplied metadata file
startInfo.ArgumentList.Add("-f");
startInfo.ArgumentList.Add("ffmetadata");
startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(ffmetaChaptersPath);
}
startInfo.ArgumentList.Add("-map"); //map file 0 (aac audio stream)
startInfo.ArgumentList.Add("0");
startInfo.ArgumentList.Add("-map_chapters"); //copy chapter data from file 1 (either metadata file or aaxc file)
startInfo.ArgumentList.Add("1");
startInfo.ArgumentList.Add("-c"); //copy all mapped streams
startInfo.ArgumentList.Add("copy");
startInfo.ArgumentList.Add("-f"); //force output format: mp4
startInfo.ArgumentList.Add("mp4");
startInfo.ArgumentList.Add("-movflags");
startInfo.ArgumentList.Add("disable_chpl"); //Disable Nero chapters format
startInfo.ArgumentList.Add(outputFile);
startInfo.ArgumentList.Add("-y"); //overwrite existing
return startInfo;
}
}
}

View File

@ -1,29 +1,34 @@
namespace AaxDecrypter 
namespace AaxDecrypter
{ {
public static class NFO public static class NFO
{ {
public static string CreateContents(string ripper, Tags tags, EncodingInfo encodingInfo, Chapters chapters) public static string CreateContents(string ripper, TagLib.File aaxcTagLib, ChapterInfo chapters)
{ {
var _hours = (int)tags.duration.TotalHours; 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 myDuration var myDuration
= (_hours > 0 ? _hours + " hours, " : "") = (_hours > 0 ? _hours + " hours, " : "")
+ tags.duration.Minutes + " minutes, " + aaxcTagLib.Properties.Duration.Minutes + " minutes, "
+ tags.duration.Seconds + " seconds"; + aaxcTagLib.Properties.Duration.Seconds + " seconds";
var header var header
= "General Information\r\n" = "General Information\r\n"
+ "===================\r\n" + "===================\r\n"
+ $" Title: {tags.title}\r\n" + $" Title: {aaxcTagLib.Tag.Title.Replace(" (Unabridged)", "")}\r\n"
+ $" Author: {tags.author}\r\n" + $" Author: {aaxcTagLib.Tag.FirstPerformer ?? "[unknown]"}\r\n"
+ $" Read By: {tags.narrator}\r\n" + $" Read By: {aaxcTagLib.GetTag(TagLib.TagTypes.Apple).Narrator??"[unknown]"}\r\n"
+ $" Copyright: {tags.year}\r\n" + $" Copyright: {aaxcTagLib.Tag.Year}\r\n"
+ $" Audiobook Copyright: {tags.year}\r\n"; + $" Audiobook Copyright: {aaxcTagLib.Tag.Year}\r\n";
if (tags.genre != "") if (!string.IsNullOrEmpty(aaxcTagLib.Tag.FirstGenre))
header += $" Genre: {tags.genre}\r\n"; header += $" Genre: {aaxcTagLib.Tag.FirstGenre}\r\n";
var s var s
= header = header
+ $" Publisher: {tags.publisher}\r\n" + $" 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"
@ -31,22 +36,22 @@
+ "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: {encodingInfo.sampleRate} Hz\r\n" + $" Source Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n"
+ $" Source Channels: {encodingInfo.channels}\r\n" + $" Source Channels: {aaxcTagLib.Properties.AudioChannels}\r\n"
+ $" Source Bitrate: {encodingInfo.originalBitrate} kbits\r\n" + $" Source Bitrate: {aaxcTagLib.Properties.AudioBitrate} kbits\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: {encodingInfo.sampleRate} Hz\r\n" + $" Encoded Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n"
+ $" Encoded Channels: {encodingInfo.channels}\r\n" + $" Encoded Channels: {aaxcTagLib.Properties.AudioChannels}\r\n"
+ $" Encoded Bitrate: {encodingInfo.originalBitrate} kbits\r\n" + $" Encoded Bitrate: {aaxcTagLib.Properties.AudioBitrate} kbits\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"
+ tags.comments; + (!string.IsNullOrWhiteSpace(tag.LongDescription) ? tag.LongDescription : tag.Description);
return s; return s;
} }

View File

@ -0,0 +1,135 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace AaxDecrypter
{
/// <summary>
/// Provides a <see cref="TagLib.File.IFileAbstraction"/> for a file over Http.
/// </summary>
class NetworkFileAbstraction : TagLib.File.IFileAbstraction
{
private NetworkFileStream aaxNetworkStream;
public static async Task<NetworkFileAbstraction> CreateAsync(HttpClient client, Uri webFileUri)
{
var response = await client.GetAsync(webFileUri, HttpCompletionOption.ResponseHeadersRead);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
throw new Exception("Can't read file from client.");
var contentLength = response.Content.Headers.ContentLength ?? 0;
var networkStream = await response.Content.ReadAsStreamAsync();
var networkFile = new NetworkFileAbstraction(Path.GetFileName(webFileUri.LocalPath), networkStream, contentLength);
return networkFile;
}
private NetworkFileAbstraction(string fileName, Stream netStream, long contentLength)
{
Name = fileName;
aaxNetworkStream = new NetworkFileStream(netStream, contentLength);
}
public string Name { get; private set; }
public Stream ReadStream => aaxNetworkStream;
public Stream WriteStream => throw new NotImplementedException();
public void CloseStream(Stream stream)
{
aaxNetworkStream.Close();
}
private class NetworkFileStream : Stream
{
private const int BUFF_SZ = 2 * 1024;
private FileStream _fileBacker;
private Stream _networkStream;
private long networkBytesRead = 0;
private long _contentLength;
public NetworkFileStream(Stream netStream, long contentLength)
{
_networkStream = netStream;
_contentLength = contentLength;
_fileBacker = File.Create(Path.GetTempFileName(), BUFF_SZ, FileOptions.DeleteOnClose);
}
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => false;
public override long Length => _contentLength;
public override long Position { get => _fileBacker.Position; set => Seek(value, 0); }
public override void Flush()
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
long requiredLength = Position + count;
if (requiredLength > networkBytesRead)
readWebFileToPosition(requiredLength);
return _fileBacker.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
long newPosition = (long)origin + offset;
if (newPosition > networkBytesRead)
readWebFileToPosition(newPosition);
_fileBacker.Position = newPosition;
return newPosition;
}
public override void Close()
{
_fileBacker.Close();
_networkStream.Close();
}
/// <summary>
/// Read more data from <see cref="_networkStream"/> into <see cref="_fileBacker"/> as needed.
/// </summary>
/// <param name="requiredLength">Length of strem required for the operation.</param>
private void readWebFileToPosition(long requiredLength)
{
byte[] buff = new byte[BUFF_SZ];
long backerPosition = _fileBacker.Position;
_fileBacker.Position = networkBytesRead;
while (networkBytesRead < requiredLength)
{
int bytesRead = _networkStream.Read(buff, 0, BUFF_SZ);
_fileBacker.Write(buff, 0, bytesRead);
networkBytesRead += bytesRead;
}
_fileBacker.Position = backerPosition;
}
}
}
}

View File

@ -1,67 +0,0 @@
using System;
using TagLib;
using TagLib.Mpeg4;
using Dinah.Core;
namespace AaxDecrypter
{
public class Tags
{
public string title { get; }
public string album { get; }
public string author { get; }
public string comments { get; }
public string narrator { get; }
public string year { get; }
public string publisher { get; }
public string id { get; }
public string genre { get; }
public TimeSpan duration { get; }
// input file
public Tags(string file)
{
using var tagLibFile = TagLib.File.Create(file, "audio/mp4", ReadStyle.Average);
title = tagLibFile.Tag.Title.Replace(" (Unabridged)", "");
album = tagLibFile.Tag.Album.Replace(" (Unabridged)", "");
author = tagLibFile.Tag.FirstPerformer ?? "[unknown]";
year = tagLibFile.Tag.Year.ToString();
comments = tagLibFile.Tag.Comment ?? "";
duration = tagLibFile.Properties.Duration;
genre = tagLibFile.Tag.FirstGenre ?? "";
var tag = tagLibFile.GetTag(TagTypes.Apple, true);
publisher = tag.Publisher ?? "";
narrator = string.IsNullOrWhiteSpace(tagLibFile.Tag.FirstComposer) ? tag.Narrator : tagLibFile.Tag.FirstComposer;
comments = !string.IsNullOrWhiteSpace(tag.LongDescription) ? tag.LongDescription : tag.Description;
id = tag.AudibleCDEK;
}
// my best guess of what this step is doing:
// re-publish the data we read from the input file => output file
public void AddAppleTags(string file)
{
using var tagLibFile = TagLib.File.Create(file, "audio/mp4", ReadStyle.Average);
var tag = (AppleTag)tagLibFile.GetTag(TagTypes.Apple, true);
tag.Publisher = publisher;
tag.LongDescription = comments;
tag.Description = comments;
tagLibFile.Save();
}
public string GenerateFfmpegTags()
=> $";FFMETADATA1"
+ $"\nmajor_brand=aax"
+ $"\nminor_version=1"
+ $"\ncompatible_brands=aax M4B mp42isom"
+ $"\ndate={year}"
+ $"\ngenre={genre}"
+ $"\ntitle={title}"
+ $"\nartist={author}"
+ $"\nalbum={album}"
+ $"\ncomposer={narrator}"
+ $"\ncomment={comments.Truncate(254)}"
+ $"\ndescription={comments}"
+ $"\n";
}
}

View File

@ -0,0 +1,16 @@
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

@ -0,0 +1,205 @@
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
using FileManager;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AaxDecrypter;
using AudibleApi;
namespace FileLiberator.AaxcDownloadDecrypt
{
public class DownloadDecryptBook : IDecryptable
{
public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> DecryptBegin;
public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered;
public event EventHandler<byte[]> CoverImageFilepathDiscovered;
public event EventHandler<int> UpdateProgress;
public event EventHandler<TimeSpan> UpdateRemainingTime;
public event EventHandler<string> DecryptCompleted;
public event EventHandler<LibraryBook> Completed;
public event EventHandler<string> StatusUpdate;
private AaxcDownloadConverter aaxcDownloader;
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
Begin?.Invoke(this, libraryBook);
try
{
if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId))
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DecryptInProgress, libraryBook);
// decrypt failed
if (outputAudioFilename is null)
return new StatusHandler { "Decrypt failed" };
// moves files and returns dest dir. Do not put inside of if(RetainAaxFiles)
_ = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
var finalAudioExists = AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
if (!finalAudioExists)
return new StatusHandler { "Cannot find final audio file after decryption" };
return new StatusHandler();
}
finally
{
Completed?.Invoke(this, libraryBook);
}
}
private async Task<string> aaxToM4bConverterDecryptAsync(string destinationDir, LibraryBook libraryBook)
{
DecryptBegin?.Invoke(this, $"Begin decrypting {libraryBook}");
try
{
validate(libraryBook);
var api = await InternalUtilities.AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
var dlLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
var aaxcDecryptDlLic = new DownloadLicense(dlLic.DownloadUrl, dlLic.AudibleKey, dlLic.AudibleIV, Resources.UserAgent);
var destinationDirectory = Path.GetDirectoryName(destinationDir);
if (Configuration.Instance.DownloadChapters)
{
var contentMetadata = await api.GetLibraryBookMetadataAsync(libraryBook.Book.AudibleProductId);
var aaxcDecryptChapters = new ChapterInfo();
foreach (var chap in contentMetadata?.ChapterInfo?.Chapters)
aaxcDecryptChapters.AddChapter(new Chapter(chap.Title, chap.StartOffsetMs, chap.LengthMs));
aaxcDownloader = await AaxcDownloadConverter.CreateAsync(destinationDirectory, aaxcDecryptDlLic, aaxcDecryptChapters);
}
else
{
aaxcDownloader = await AaxcDownloadConverter.CreateAsync(destinationDirectory, aaxcDecryptDlLic);
}
aaxcDownloader.AppName = "Libation";
TitleDiscovered?.Invoke(this, aaxcDownloader.Title);
AuthorsDiscovered?.Invoke(this, aaxcDownloader.Author);
NarratorsDiscovered?.Invoke(this, aaxcDownloader.Narrator);
CoverImageFilepathDiscovered?.Invoke(this, aaxcDownloader.CoverArt);
// override default which was set in CreateAsync
var proposedOutputFile = Path.Combine(destinationDir, $"{libraryBook.Book.Title} [{libraryBook.Book.AudibleProductId}].m4b");
aaxcDownloader.SetOutputFilename(proposedOutputFile);
aaxcDownloader.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress);
aaxcDownloader.DecryptTimeRemaining += (s, remaining) => UpdateRemainingTime?.Invoke(this, remaining);
// REAL WORK DONE HERE
var success = await Task.Run(() => aaxcDownloader.Run());
// decrypt failed
if (!success)
return null;
return aaxcDownloader.outputFileName;
}
finally
{
DecryptCompleted?.Invoke(this, $"Completed downloading and decrypting {libraryBook.Book.Title}");
}
}
private static string moveFilesToBooksDir(Book product, string outputAudioFilename)
{
// create final directory. move each file into it. MOVE AUDIO FILE LAST
// new dir: safetitle_limit50char + " [" + productId + "]"
var destinationDir = AudibleFileStorage.Audio.GetDestDir(product.Title, product.AudibleProductId);
Directory.CreateDirectory(destinationDir);
var sortedFiles = getProductFilesSorted(product, outputAudioFilename);
var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.');
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId);
foreach (var f in sortedFiles)
{
var dest
= AudibleFileStorage.Audio.IsFileTypeMatch(f)
? audioFileName
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext
: FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
Cue.UpdateFileName(f, audioFileName);
File.Move(f.FullName, dest);
}
return destinationDir;
}
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
{
// files are: temp path\author\[asin].ext
var m4bDir = new FileInfo(outputAudioFilename).Directory;
var files = m4bDir
.EnumerateFiles()
.Where(f => f.Name.ContainsInsensitive(product.AudibleProductId))
.ToList();
// move audio files to the end of the collection so these files are moved last
var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f));
var sortedFiles = files
.Except(musicFiles)
.Concat(musicFiles)
.ToList();
return sortedFiles;
}
private static void validate(LibraryBook libraryBook)
{
string errorString(string field)
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
string errorTitle()
{
var title
= (libraryBook.Book.Title.Length > 53)
? $"{libraryBook.Book.Title.Truncate(50)}..."
: libraryBook.Book.Title;
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
return errorBookTitle;
};
if (string.IsNullOrWhiteSpace(libraryBook.Account))
throw new Exception(errorString("Account"));
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
throw new Exception(errorString("Locale"));
}
public bool Validate(LibraryBook libraryBook)
=> !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId)
&& !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId);
public void Cancel()
{
aaxcDownloader.Cancel();
}
}
}

View File

@ -3,6 +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 FileManager; using FileManager;
namespace FileLiberator namespace FileLiberator
@ -21,8 +22,9 @@ namespace FileLiberator
public event EventHandler<string> StatusUpdate; public event EventHandler<string> StatusUpdate;
public event EventHandler<LibraryBook> Completed; public event EventHandler<LibraryBook> Completed;
public DownloadBook DownloadBook { get; } = new DownloadBook();
public DecryptBook DecryptBook { get; } = new DecryptBook(); public DownloadBookDummy DownloadBook { get; } = new DownloadBookDummy();
public DownloadDecryptBook DecryptBook { get; } = new DownloadDecryptBook();
public DownloadPdf DownloadPdf { get; } = new DownloadPdf(); public DownloadPdf DownloadPdf { get; } = new DownloadPdf();
public bool Validate(LibraryBook libraryBook) public bool Validate(LibraryBook libraryBook)

View File

@ -1,207 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AaxDecrypter;
using AudibleApi;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
using FileManager;
using InternalUtilities;
namespace FileLiberator
{
/// <summary>
/// Decrypt audiobook files
///
/// Processes:
/// Download: download aax file: the DRM encrypted audiobook
/// Decrypt: remove DRM encryption from audiobook. Store final book
/// Backup: perform all steps (downloaded, decrypt) still needed to get final book
/// </summary>
public class DecryptBook : IDecryptable
{
public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> StatusUpdate;
public event EventHandler<string> DecryptBegin;
public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered;
public event EventHandler<byte[]> CoverImageFilepathDiscovered;
public event EventHandler<int> UpdateProgress;
public event EventHandler<string> DecryptCompleted;
public event EventHandler<LibraryBook> Completed;
public bool Validate(LibraryBook libraryBook)
=> AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId)
&& !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
// 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
{
var aaxFilename = AudibleFileStorage.AAX.GetPath(libraryBook.Book.AudibleProductId);
if (aaxFilename == null)
return new StatusHandler { "aaxFilename parameter is null" };
if (!File.Exists(aaxFilename))
return new StatusHandler { $"Cannot find AAX file: {aaxFilename}" };
if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId))
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
var chapters = await downloadChapterNamesAsync(libraryBook);
var outputAudioFilename = await aaxToM4bConverterDecryptAsync(aaxFilename, libraryBook, chapters);
// decrypt failed
if (outputAudioFilename == null)
return new StatusHandler { "Decrypt failed" };
// moves files and returns dest dir. Do not put inside of if(RetainAaxFiles)
var destinationDir = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
var jsonFilename = PathLib.ReplaceExtension(aaxFilename, "json");
if (Configuration.Instance.RetainAaxFiles)
{
var newAaxFilename = FileUtility.GetValidFilename(
destinationDir,
Path.GetFileNameWithoutExtension(aaxFilename),
"aax");
File.Move(aaxFilename, newAaxFilename);
var newJsonFilename = PathLib.ReplaceExtension(newAaxFilename, "json");
File.Move(jsonFilename, newJsonFilename);
}
else
{
Dinah.Core.IO.FileExt.SafeDelete(aaxFilename);
Dinah.Core.IO.FileExt.SafeDelete(jsonFilename);
}
var finalAudioExists = AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
if (!finalAudioExists)
return new StatusHandler { "Cannot find final audio file after decryption" };
return new StatusHandler();
}
finally
{
Completed?.Invoke(this, libraryBook);
}
}
private static async Task<Chapters> downloadChapterNamesAsync(LibraryBook libraryBook)
{
try
{
var api = await AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
var contentMetadata = await api.GetLibraryBookMetadataAsync(libraryBook.Book.AudibleProductId);
if (contentMetadata?.ChapterInfo is null)
return null;
return new DownloadedChapters(contentMetadata.ChapterInfo);
}
catch
{
return null;
}
}
private async Task<string> aaxToM4bConverterDecryptAsync(string aaxFilename, LibraryBook libraryBook, Chapters chapters)
{
DecryptBegin?.Invoke(this, $"Begin decrypting {aaxFilename}");
try
{
var jsonPath = PathLib.ReplaceExtension(aaxFilename, "json");
var jsonContents = File.ReadAllText(jsonPath);
var dlLic = Newtonsoft.Json.JsonConvert.DeserializeObject<AudibleApiDTOs.DownloadLicense>(jsonContents);
var converter = await AaxToM4bConverter.CreateAsync(aaxFilename, dlLic.AudibleKey, dlLic.AudibleIV, chapters);
converter.AppName = "Libation";
TitleDiscovered?.Invoke(this, converter.tags.title);
AuthorsDiscovered?.Invoke(this, converter.tags.author);
NarratorsDiscovered?.Invoke(this, converter.tags.narrator);
CoverImageFilepathDiscovered?.Invoke(this, converter.coverBytes);
// override default which was set in CreateAsync
var proposedOutputFile = Path.Combine(AudibleFileStorage.DecryptInProgress, $"[{libraryBook.Book.AudibleProductId}].m4b");
converter.SetOutputFilename(proposedOutputFile);
converter.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress);
// REAL WORK DONE HERE
var success = await Task.Run(() => converter.Run());
// decrypt failed
if (!success)
return null;
return converter.outputFileName;
}
finally
{
DecryptCompleted?.Invoke(this, $"Completed decrypting {aaxFilename}");
}
}
private static string moveFilesToBooksDir(Book product, string outputAudioFilename)
{
// create final directory. move each file into it. MOVE AUDIO FILE LAST
// new dir: safetitle_limit50char + " [" + productId + "]"
var destinationDir = AudibleFileStorage.Audio.GetDestDir(product.Title, product.AudibleProductId);
Directory.CreateDirectory(destinationDir);
var sortedFiles = getProductFilesSorted(product, outputAudioFilename);
var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.');
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId);
foreach (var f in sortedFiles)
{
var dest
= AudibleFileStorage.Audio.IsFileTypeMatch(f)
? audioFileName
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext
: FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
Cue.UpdateFileName(f, audioFileName);
File.Move(f.FullName, dest);
}
return destinationDir;
}
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
{
// files are: temp path\author\[asin].ext
var m4bDir = new FileInfo(outputAudioFilename).Directory;
var files = m4bDir
.EnumerateFiles()
.Where(f => f.Name.ContainsInsensitive(product.AudibleProductId))
.ToList();
// move audio files to the end of the collection so these files are moved last
var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f));
var sortedFiles = files
.Except(musicFiles)
.Concat(musicFiles)
.ToList();
return sortedFiles;
}
}
}

View File

@ -1,143 +0,0 @@
using System;
using System.IO;
using System.Threading.Tasks;
using AudibleApi;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
using FileManager;
using System.Net.Http;
using Dinah.Core.Net.Http;
namespace FileLiberator
{
/// <summary>
/// Download DRM book
///
/// Processes:
/// Download: download aax file: the DRM encrypted audiobook
/// Decrypt: remove DRM encryption from audiobook. Store final book
/// Backup: perform all steps (downloaded, decrypt) still needed to get final book
/// </summary>
public class DownloadBook : DownloadableBase
{
private const string SERVICE_UNAVAILABLE = "Content Delivery Companion Service is not available.";
public override bool Validate(LibraryBook libraryBook)
=> !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId)
&& !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId);
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
{
var tempAaxFilename = getDownloadPath(libraryBook);
var actualFilePath = await downloadAaxcBookAsync(libraryBook, tempAaxFilename);
moveBook(libraryBook, actualFilePath);
return verifyDownload(libraryBook);
}
private static string getDownloadPath(LibraryBook libraryBook)
=> FileUtility.GetValidFilename(
AudibleFileStorage.DownloadsInProgress,
libraryBook.Book.Title,
"aaxc",
libraryBook.Book.AudibleProductId);
private async Task<string> downloadAaxcBookAsync(LibraryBook libraryBook, string tempAaxFilename)
{
validate(libraryBook);
var api = await GetApiAsync(libraryBook);
var dlLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
var client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", Resources.UserAgent);
var actualFilePath = await PerformDownloadAsync(
tempAaxFilename,
(p) => client.DownloadFileAsync(new Uri(dlLic.DownloadUrl).AbsoluteUri, tempAaxFilename, p));
System.Threading.Thread.Sleep(100);
// if bad file download, a 0-33 byte file will be created
// if service unavailable, a 52 byte string will be saved as file
var length = new FileInfo(actualFilePath).Length;
// success. save json and return
if (length > 100)
{
// save along side book
var jsonPath = PathLib.ReplaceExtension(actualFilePath, "json");
var jsonContents = Newtonsoft.Json.JsonConvert.SerializeObject(dlLic, Newtonsoft.Json.Formatting.Indented);
File.WriteAllText(jsonPath, jsonContents);
return actualFilePath;
}
// else: failure. clean up and throw
var contents = File.ReadAllText(actualFilePath);
File.Delete(actualFilePath);
var exMsg = contents.StartsWithInsensitive(SERVICE_UNAVAILABLE)
? SERVICE_UNAVAILABLE
: "Error downloading file";
var ex = new Exception(exMsg);
Serilog.Log.Logger.Error(ex, "Download error {@DebugInfo}", new
{
libraryBook.Book.Title,
libraryBook.Book.AudibleProductId,
libraryBook.Book.Locale,
Account = libraryBook.Account?.ToMask() ?? "[empty]",
tempAaxFilename,
actualFilePath,
length,
contents
});
throw ex;
}
private static void validate(LibraryBook libraryBook)
{
string errorString(string field)
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
string errorTitle()
{
var title
= (libraryBook.Book.Title.Length > 53)
? $"{libraryBook.Book.Title.Truncate(50)}..."
: libraryBook.Book.Title;
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
return errorBookTitle;
};
if (string.IsNullOrWhiteSpace(libraryBook.Account))
throw new Exception(errorString("Account"));
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
throw new Exception(errorString("Locale"));
}
private void moveBook(LibraryBook libraryBook, string actualFilePath)
{
var newAaxFilename = FileUtility.GetValidFilename(
AudibleFileStorage.DownloadsFinal,
libraryBook.Book.Title,
"aax",
libraryBook.Book.AudibleProductId);
File.Move(actualFilePath, newAaxFilename);
// also move DownloadLicense json file
var jsonPathOld = PathLib.ReplaceExtension(actualFilePath, "json");
var jsonPathNew = PathLib.ReplaceExtension(newAaxFilename, "json");
File.Move(jsonPathOld, jsonPathNew);
Invoke_StatusUpdate($"Successfully downloaded. Moved to: {newAaxFilename}");
}
private static StatusHandler verifyDownload(LibraryBook libraryBook)
=> !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId)
? new StatusHandler { "Downloaded AAX file cannot be found" }
: new StatusHandler();
}
}

View File

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using AaxDecrypter;
using AudibleApiDTOs;
using Dinah.Core.Diagnostics;
namespace FileLiberator
{
public class DownloadedChapters : Chapters
{
public DownloadedChapters(ChapterInfo chapterInfo)
{
AddChapters(chapterInfo.Chapters
.Select(c => new AaxDecrypter.Chapter(c.StartOffsetMs / 1000d, (c.StartOffsetMs + c.LengthMs) / 1000d, c.Title)));
}
}
}

View File

@ -11,7 +11,9 @@ namespace FileLiberator
event EventHandler<string> NarratorsDiscovered; event EventHandler<string> NarratorsDiscovered;
event EventHandler<byte[]> CoverImageFilepathDiscovered; event EventHandler<byte[]> CoverImageFilepathDiscovered;
event EventHandler<int> UpdateProgress; event EventHandler<int> UpdateProgress;
event EventHandler<TimeSpan> UpdateRemainingTime;
event EventHandler<string> DecryptCompleted; event EventHandler<string> DecryptCompleted;
void Cancel();
} }
} }

View File

@ -83,13 +83,12 @@ namespace FileManager
set => persistentDictionary.Set(nameof(DecryptInProgressEnum), value); set => persistentDictionary.Set(nameof(DecryptInProgressEnum), value);
} }
[Description("Retain .aax files after decrypting?")] [Description("Download chapter titles from Audible?")]
public bool RetainAaxFiles public bool DownloadChapters
{ {
get => persistentDictionary.Get<bool>(nameof(RetainAaxFiles)); get => persistentDictionary.Get<bool>(nameof(DownloadChapters));
set => persistentDictionary.Set(nameof(RetainAaxFiles), value); set => persistentDictionary.Set(nameof(DownloadChapters), 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
// singleton stuff // singleton stuff

View File

@ -13,7 +13,7 @@
<!-- <PublishSingleFile>true</PublishSingleFile> --> <!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>4.4.0.64</Version> <Version>4.4.0.181</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -60,6 +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;
}; };
// setupDialog.BasicBtn_Click += (_, __) => // no action needed // setupDialog.BasicBtn_Click += (_, __) => // no action needed
setupDialog.AdvancedBtn_Click += (_, __) => isAdvanced = true; setupDialog.AdvancedBtn_Click += (_, __) => isAdvanced = true;
@ -232,6 +233,13 @@ namespace LibationLauncher
#region migrate_to_v5_0_0 re-gegister device if device info not in settings #region migrate_to_v5_0_0 re-gegister device if device info not in settings
private static void migrate_to_v5_0_0() private static void migrate_to_v5_0_0()
{ {
var persistentDictionary = new PersistentDictionary(Configuration.Instance.SettingsFilePath);
if (persistentDictionary.GetString("DownloadChapters") is null)
{
persistentDictionary.Set("DownloadChapters", true);
}
if (!File.Exists(AudibleApiStorage.AccountsSettingsFile)) if (!File.Exists(AudibleApiStorage.AccountsSettingsFile))
return; return;

View File

@ -32,14 +32,16 @@
this.bookInfoLbl = new System.Windows.Forms.Label(); this.bookInfoLbl = new System.Windows.Forms.Label();
this.progressBar1 = new System.Windows.Forms.ProgressBar(); this.progressBar1 = new System.Windows.Forms.ProgressBar();
this.rtbLog = new System.Windows.Forms.RichTextBox(); this.rtbLog = new System.Windows.Forms.RichTextBox();
this.remainingTimeLbl = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
this.SuspendLayout(); this.SuspendLayout();
// //
// pictureBox1 // pictureBox1
// //
this.pictureBox1.Location = new System.Drawing.Point(12, 12); this.pictureBox1.Location = new System.Drawing.Point(14, 14);
this.pictureBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.pictureBox1.Name = "pictureBox1"; this.pictureBox1.Name = "pictureBox1";
this.pictureBox1.Size = new System.Drawing.Size(100, 100); this.pictureBox1.Size = new System.Drawing.Size(117, 115);
this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage; this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
this.pictureBox1.TabIndex = 0; this.pictureBox1.TabIndex = 0;
this.pictureBox1.TabStop = false; this.pictureBox1.TabStop = false;
@ -47,9 +49,10 @@
// bookInfoLbl // bookInfoLbl
// //
this.bookInfoLbl.AutoSize = true; this.bookInfoLbl.AutoSize = true;
this.bookInfoLbl.Location = new System.Drawing.Point(118, 12); this.bookInfoLbl.Location = new System.Drawing.Point(138, 14);
this.bookInfoLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.bookInfoLbl.Name = "bookInfoLbl"; this.bookInfoLbl.Name = "bookInfoLbl";
this.bookInfoLbl.Size = new System.Drawing.Size(100, 13); this.bookInfoLbl.Size = new System.Drawing.Size(121, 15);
this.bookInfoLbl.TabIndex = 0; this.bookInfoLbl.TabIndex = 0;
this.bookInfoLbl.Text = "[multi-line book info]"; this.bookInfoLbl.Text = "[multi-line book info]";
// //
@ -57,9 +60,10 @@
// //
this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right))); | System.Windows.Forms.AnchorStyles.Right)));
this.progressBar1.Location = new System.Drawing.Point(12, 526); this.progressBar1.Location = new System.Drawing.Point(14, 607);
this.progressBar1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.progressBar1.Name = "progressBar1"; this.progressBar1.Name = "progressBar1";
this.progressBar1.Size = new System.Drawing.Size(582, 23); this.progressBar1.Size = new System.Drawing.Size(611, 27);
this.progressBar1.TabIndex = 2; this.progressBar1.TabIndex = 2;
// //
// rtbLog // rtbLog
@ -67,21 +71,33 @@
this.rtbLog.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) this.rtbLog.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right))); | System.Windows.Forms.AnchorStyles.Right)));
this.rtbLog.Location = new System.Drawing.Point(12, 118); this.rtbLog.Location = new System.Drawing.Point(14, 136);
this.rtbLog.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.rtbLog.Name = "rtbLog"; this.rtbLog.Name = "rtbLog";
this.rtbLog.Size = new System.Drawing.Size(582, 402); this.rtbLog.Size = new System.Drawing.Size(678, 463);
this.rtbLog.TabIndex = 1; this.rtbLog.TabIndex = 1;
this.rtbLog.Text = ""; this.rtbLog.Text = "";
// //
// remainingTimeLbl
//
this.remainingTimeLbl.Location = new System.Drawing.Point(632, 607);
this.remainingTimeLbl.Name = "remainingTimeLbl";
this.remainingTimeLbl.Size = new System.Drawing.Size(60, 31);
this.remainingTimeLbl.TabIndex = 3;
this.remainingTimeLbl.Text = "ETA:\r\n";
this.remainingTimeLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
// DecryptForm // DecryptForm
// //
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(606, 561); this.ClientSize = new System.Drawing.Size(707, 647);
this.Controls.Add(this.remainingTimeLbl);
this.Controls.Add(this.rtbLog); this.Controls.Add(this.rtbLog);
this.Controls.Add(this.progressBar1); this.Controls.Add(this.progressBar1);
this.Controls.Add(this.bookInfoLbl); this.Controls.Add(this.bookInfoLbl);
this.Controls.Add(this.pictureBox1); this.Controls.Add(this.pictureBox1);
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "DecryptForm"; this.Name = "DecryptForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "DecryptForm"; this.Text = "DecryptForm";
@ -99,5 +115,6 @@
private System.Windows.Forms.Label bookInfoLbl; private System.Windows.Forms.Label bookInfoLbl;
private System.Windows.Forms.ProgressBar progressBar1; private System.Windows.Forms.ProgressBar progressBar1;
private System.Windows.Forms.RichTextBox rtbLog; private System.Windows.Forms.RichTextBox rtbLog;
private System.Windows.Forms.Label remainingTimeLbl;
} }
} }

View File

@ -59,6 +59,17 @@ namespace LibationWinForms.BookLiberation
public void SetCoverImage(byte[] coverBytes) public void SetCoverImage(byte[] coverBytes)
=> pictureBox1.UIThread(() => pictureBox1.Image = ImageReader.ToImage(coverBytes)); => pictureBox1.UIThread(() => pictureBox1.Image = ImageReader.ToImage(coverBytes));
public void UpdateProgress(int percentage) => progressBar1.UIThread(() => progressBar1.Value = percentage); public void UpdateProgress(int percentage)
{
if (percentage == 0)
remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = "ETA:\r\n0 sec");
progressBar1.UIThread(() => progressBar1.Value = percentage);
}
public void UpdateRemainingTime(TimeSpan remaining)
{
remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = $"ETA:\r\n{(int)remaining.TotalSeconds} sec");
}
} }
} }

View File

@ -1,64 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <root>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<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">

View File

@ -264,6 +264,7 @@ namespace LibationWinForms.BookLiberation
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(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 decryptCompleted(object _, string __) => decryptDialog.Close(); void decryptCompleted(object _, string __) => decryptDialog.Close();
#endregion #endregion
@ -276,6 +277,7 @@ namespace LibationWinForms.BookLiberation
decryptBook.NarratorsDiscovered += narratorsDiscovered; decryptBook.NarratorsDiscovered += narratorsDiscovered;
decryptBook.CoverImageFilepathDiscovered += coverImageFilepathDiscovered; decryptBook.CoverImageFilepathDiscovered += coverImageFilepathDiscovered;
decryptBook.UpdateProgress += updateProgress; decryptBook.UpdateProgress += updateProgress;
decryptBook.UpdateRemainingTime += updateRemainingTime;
decryptBook.DecryptCompleted += decryptCompleted; decryptBook.DecryptCompleted += decryptCompleted;
#endregion #endregion
@ -291,8 +293,10 @@ namespace LibationWinForms.BookLiberation
decryptBook.NarratorsDiscovered -= narratorsDiscovered; decryptBook.NarratorsDiscovered -= narratorsDiscovered;
decryptBook.CoverImageFilepathDiscovered -= coverImageFilepathDiscovered; decryptBook.CoverImageFilepathDiscovered -= coverImageFilepathDiscovered;
decryptBook.UpdateProgress -= updateProgress; decryptBook.UpdateProgress -= updateProgress;
decryptBook.UpdateRemainingTime -= updateRemainingTime;
decryptBook.DecryptCompleted -= decryptCompleted; decryptBook.DecryptCompleted -= decryptCompleted;
decryptBook.Cancel();
}; };
#endregion #endregion
} }

View File

@ -43,6 +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.downloadsInProgressGb.SuspendLayout(); this.downloadsInProgressGb.SuspendLayout();
this.decryptInProgressGb.SuspendLayout(); this.decryptInProgressGb.SuspendLayout();
this.groupBox1.SuspendLayout(); this.groupBox1.SuspendLayout();
@ -51,24 +52,27 @@
// booksLocationLbl // booksLocationLbl
// //
this.booksLocationLbl.AutoSize = true; this.booksLocationLbl.AutoSize = true;
this.booksLocationLbl.Location = new System.Drawing.Point(12, 17); this.booksLocationLbl.Location = new System.Drawing.Point(14, 20);
this.booksLocationLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.booksLocationLbl.Name = "booksLocationLbl"; this.booksLocationLbl.Name = "booksLocationLbl";
this.booksLocationLbl.Size = new System.Drawing.Size(77, 13); this.booksLocationLbl.Size = new System.Drawing.Size(85, 15);
this.booksLocationLbl.TabIndex = 0; this.booksLocationLbl.TabIndex = 0;
this.booksLocationLbl.Text = "Books location"; this.booksLocationLbl.Text = "Books location";
// //
// booksLocationTb // booksLocationTb
// //
this.booksLocationTb.Location = new System.Drawing.Point(95, 14); this.booksLocationTb.Location = new System.Drawing.Point(111, 16);
this.booksLocationTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.booksLocationTb.Name = "booksLocationTb"; this.booksLocationTb.Name = "booksLocationTb";
this.booksLocationTb.Size = new System.Drawing.Size(652, 20); this.booksLocationTb.Size = new System.Drawing.Size(760, 23);
this.booksLocationTb.TabIndex = 1; this.booksLocationTb.TabIndex = 1;
// //
// booksLocationSearchBtn // booksLocationSearchBtn
// //
this.booksLocationSearchBtn.Location = new System.Drawing.Point(753, 12); this.booksLocationSearchBtn.Location = new System.Drawing.Point(878, 14);
this.booksLocationSearchBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.booksLocationSearchBtn.Name = "booksLocationSearchBtn"; this.booksLocationSearchBtn.Name = "booksLocationSearchBtn";
this.booksLocationSearchBtn.Size = new System.Drawing.Size(35, 23); this.booksLocationSearchBtn.Size = new System.Drawing.Size(41, 27);
this.booksLocationSearchBtn.TabIndex = 2; this.booksLocationSearchBtn.TabIndex = 2;
this.booksLocationSearchBtn.Text = "..."; this.booksLocationSearchBtn.Text = "...";
this.booksLocationSearchBtn.UseVisualStyleBackColor = true; this.booksLocationSearchBtn.UseVisualStyleBackColor = true;
@ -77,9 +81,10 @@
// booksLocationDescLbl // booksLocationDescLbl
// //
this.booksLocationDescLbl.AutoSize = true; this.booksLocationDescLbl.AutoSize = true;
this.booksLocationDescLbl.Location = new System.Drawing.Point(92, 37); this.booksLocationDescLbl.Location = new System.Drawing.Point(107, 43);
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(36, 13); this.booksLocationDescLbl.Size = new System.Drawing.Size(39, 15);
this.booksLocationDescLbl.TabIndex = 3; this.booksLocationDescLbl.TabIndex = 3;
this.booksLocationDescLbl.Text = "[desc]"; this.booksLocationDescLbl.Text = "[desc]";
// //
@ -88,9 +93,11 @@
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressLibationFilesRb); this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressLibationFilesRb);
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressWinTempRb); this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressWinTempRb);
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressDescLbl); this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressDescLbl);
this.downloadsInProgressGb.Location = new System.Drawing.Point(15, 19); this.downloadsInProgressGb.Location = new System.Drawing.Point(10, 49);
this.downloadsInProgressGb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.downloadsInProgressGb.Name = "downloadsInProgressGb"; this.downloadsInProgressGb.Name = "downloadsInProgressGb";
this.downloadsInProgressGb.Size = new System.Drawing.Size(758, 117); this.downloadsInProgressGb.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.downloadsInProgressGb.Size = new System.Drawing.Size(884, 135);
this.downloadsInProgressGb.TabIndex = 4; this.downloadsInProgressGb.TabIndex = 4;
this.downloadsInProgressGb.TabStop = false; this.downloadsInProgressGb.TabStop = false;
this.downloadsInProgressGb.Text = "Downloads in progress"; this.downloadsInProgressGb.Text = "Downloads in progress";
@ -99,9 +106,10 @@
// //
this.downloadsInProgressLibationFilesRb.AutoSize = true; this.downloadsInProgressLibationFilesRb.AutoSize = true;
this.downloadsInProgressLibationFilesRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft; this.downloadsInProgressLibationFilesRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft;
this.downloadsInProgressLibationFilesRb.Location = new System.Drawing.Point(9, 81); this.downloadsInProgressLibationFilesRb.Location = new System.Drawing.Point(10, 93);
this.downloadsInProgressLibationFilesRb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.downloadsInProgressLibationFilesRb.Name = "downloadsInProgressLibationFilesRb"; this.downloadsInProgressLibationFilesRb.Name = "downloadsInProgressLibationFilesRb";
this.downloadsInProgressLibationFilesRb.Size = new System.Drawing.Size(193, 30); this.downloadsInProgressLibationFilesRb.Size = new System.Drawing.Size(215, 34);
this.downloadsInProgressLibationFilesRb.TabIndex = 2; this.downloadsInProgressLibationFilesRb.TabIndex = 2;
this.downloadsInProgressLibationFilesRb.TabStop = true; this.downloadsInProgressLibationFilesRb.TabStop = true;
this.downloadsInProgressLibationFilesRb.Text = "[desc]\r\n[libationFiles\\DownloadsInProgress]"; this.downloadsInProgressLibationFilesRb.Text = "[desc]\r\n[libationFiles\\DownloadsInProgress]";
@ -111,9 +119,10 @@
// //
this.downloadsInProgressWinTempRb.AutoSize = true; this.downloadsInProgressWinTempRb.AutoSize = true;
this.downloadsInProgressWinTempRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft; this.downloadsInProgressWinTempRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft;
this.downloadsInProgressWinTempRb.Location = new System.Drawing.Point(9, 45); this.downloadsInProgressWinTempRb.Location = new System.Drawing.Point(10, 52);
this.downloadsInProgressWinTempRb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.downloadsInProgressWinTempRb.Name = "downloadsInProgressWinTempRb"; this.downloadsInProgressWinTempRb.Name = "downloadsInProgressWinTempRb";
this.downloadsInProgressWinTempRb.Size = new System.Drawing.Size(182, 30); this.downloadsInProgressWinTempRb.Size = new System.Drawing.Size(200, 34);
this.downloadsInProgressWinTempRb.TabIndex = 1; this.downloadsInProgressWinTempRb.TabIndex = 1;
this.downloadsInProgressWinTempRb.TabStop = true; this.downloadsInProgressWinTempRb.TabStop = true;
this.downloadsInProgressWinTempRb.Text = "[desc]\r\n[winTemp\\DownloadsInProgress]"; this.downloadsInProgressWinTempRb.Text = "[desc]\r\n[winTemp\\DownloadsInProgress]";
@ -122,9 +131,10 @@
// downloadsInProgressDescLbl // downloadsInProgressDescLbl
// //
this.downloadsInProgressDescLbl.AutoSize = true; this.downloadsInProgressDescLbl.AutoSize = true;
this.downloadsInProgressDescLbl.Location = new System.Drawing.Point(6, 16); this.downloadsInProgressDescLbl.Location = new System.Drawing.Point(7, 18);
this.downloadsInProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.downloadsInProgressDescLbl.Name = "downloadsInProgressDescLbl"; this.downloadsInProgressDescLbl.Name = "downloadsInProgressDescLbl";
this.downloadsInProgressDescLbl.Size = new System.Drawing.Size(38, 26); this.downloadsInProgressDescLbl.Size = new System.Drawing.Size(43, 30);
this.downloadsInProgressDescLbl.TabIndex = 0; this.downloadsInProgressDescLbl.TabIndex = 0;
this.downloadsInProgressDescLbl.Text = "[desc]\r\n[line 2]"; this.downloadsInProgressDescLbl.Text = "[desc]\r\n[line 2]";
// //
@ -133,9 +143,11 @@
this.decryptInProgressGb.Controls.Add(this.decryptInProgressLibationFilesRb); this.decryptInProgressGb.Controls.Add(this.decryptInProgressLibationFilesRb);
this.decryptInProgressGb.Controls.Add(this.decryptInProgressWinTempRb); this.decryptInProgressGb.Controls.Add(this.decryptInProgressWinTempRb);
this.decryptInProgressGb.Controls.Add(this.decryptInProgressDescLbl); this.decryptInProgressGb.Controls.Add(this.decryptInProgressDescLbl);
this.decryptInProgressGb.Location = new System.Drawing.Point(9, 144); this.decryptInProgressGb.Location = new System.Drawing.Point(10, 193);
this.decryptInProgressGb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.decryptInProgressGb.Name = "decryptInProgressGb"; this.decryptInProgressGb.Name = "decryptInProgressGb";
this.decryptInProgressGb.Size = new System.Drawing.Size(758, 117); this.decryptInProgressGb.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.decryptInProgressGb.Size = new System.Drawing.Size(884, 135);
this.decryptInProgressGb.TabIndex = 5; this.decryptInProgressGb.TabIndex = 5;
this.decryptInProgressGb.TabStop = false; this.decryptInProgressGb.TabStop = false;
this.decryptInProgressGb.Text = "Decrypt in progress"; this.decryptInProgressGb.Text = "Decrypt in progress";
@ -144,9 +156,10 @@
// //
this.decryptInProgressLibationFilesRb.AutoSize = true; this.decryptInProgressLibationFilesRb.AutoSize = true;
this.decryptInProgressLibationFilesRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft; this.decryptInProgressLibationFilesRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft;
this.decryptInProgressLibationFilesRb.Location = new System.Drawing.Point(6, 81); this.decryptInProgressLibationFilesRb.Location = new System.Drawing.Point(7, 93);
this.decryptInProgressLibationFilesRb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.decryptInProgressLibationFilesRb.Name = "decryptInProgressLibationFilesRb"; this.decryptInProgressLibationFilesRb.Name = "decryptInProgressLibationFilesRb";
this.decryptInProgressLibationFilesRb.Size = new System.Drawing.Size(177, 30); this.decryptInProgressLibationFilesRb.Size = new System.Drawing.Size(197, 34);
this.decryptInProgressLibationFilesRb.TabIndex = 2; this.decryptInProgressLibationFilesRb.TabIndex = 2;
this.decryptInProgressLibationFilesRb.TabStop = true; this.decryptInProgressLibationFilesRb.TabStop = true;
this.decryptInProgressLibationFilesRb.Text = "[desc]\r\n[libationFiles\\DecryptInProgress]"; this.decryptInProgressLibationFilesRb.Text = "[desc]\r\n[libationFiles\\DecryptInProgress]";
@ -156,9 +169,10 @@
// //
this.decryptInProgressWinTempRb.AutoSize = true; this.decryptInProgressWinTempRb.AutoSize = true;
this.decryptInProgressWinTempRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft; this.decryptInProgressWinTempRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft;
this.decryptInProgressWinTempRb.Location = new System.Drawing.Point(6, 45); this.decryptInProgressWinTempRb.Location = new System.Drawing.Point(7, 52);
this.decryptInProgressWinTempRb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.decryptInProgressWinTempRb.Name = "decryptInProgressWinTempRb"; this.decryptInProgressWinTempRb.Name = "decryptInProgressWinTempRb";
this.decryptInProgressWinTempRb.Size = new System.Drawing.Size(166, 30); this.decryptInProgressWinTempRb.Size = new System.Drawing.Size(182, 34);
this.decryptInProgressWinTempRb.TabIndex = 1; this.decryptInProgressWinTempRb.TabIndex = 1;
this.decryptInProgressWinTempRb.TabStop = true; this.decryptInProgressWinTempRb.TabStop = true;
this.decryptInProgressWinTempRb.Text = "[desc]\r\n[winTemp\\DecryptInProgress]"; this.decryptInProgressWinTempRb.Text = "[desc]\r\n[winTemp\\DecryptInProgress]";
@ -167,18 +181,20 @@
// decryptInProgressDescLbl // decryptInProgressDescLbl
// //
this.decryptInProgressDescLbl.AutoSize = true; this.decryptInProgressDescLbl.AutoSize = true;
this.decryptInProgressDescLbl.Location = new System.Drawing.Point(6, 16); this.decryptInProgressDescLbl.Location = new System.Drawing.Point(7, 18);
this.decryptInProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.decryptInProgressDescLbl.Name = "decryptInProgressDescLbl"; this.decryptInProgressDescLbl.Name = "decryptInProgressDescLbl";
this.decryptInProgressDescLbl.Size = new System.Drawing.Size(38, 26); this.decryptInProgressDescLbl.Size = new System.Drawing.Size(43, 30);
this.decryptInProgressDescLbl.TabIndex = 0; this.decryptInProgressDescLbl.TabIndex = 0;
this.decryptInProgressDescLbl.Text = "[desc]\r\n[line 2]"; this.decryptInProgressDescLbl.Text = "[desc]\r\n[line 2]";
// //
// 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(612, 328); this.saveBtn.Location = new System.Drawing.Point(714, 401);
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(75, 23); this.saveBtn.Size = new System.Drawing.Size(88, 27);
this.saveBtn.TabIndex = 7; this.saveBtn.TabIndex = 7;
this.saveBtn.Text = "Save"; this.saveBtn.Text = "Save";
this.saveBtn.UseVisualStyleBackColor = true; this.saveBtn.UseVisualStyleBackColor = true;
@ -188,9 +204,10 @@
// //
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(713, 328); this.cancelBtn.Location = new System.Drawing.Point(832, 401);
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(75, 23); this.cancelBtn.Size = new System.Drawing.Size(88, 27);
this.cancelBtn.TabIndex = 8; this.cancelBtn.TabIndex = 8;
this.cancelBtn.Text = "Cancel"; this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true; this.cancelBtn.UseVisualStyleBackColor = true;
@ -198,22 +215,35 @@
// //
// groupBox1 // groupBox1
// //
this.groupBox1.Controls.Add(this.downloadChaptersCbox);
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(15, 53); this.groupBox1.Location = new System.Drawing.Point(18, 61);
this.groupBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.groupBox1.Name = "groupBox1"; this.groupBox1.Name = "groupBox1";
this.groupBox1.Size = new System.Drawing.Size(773, 269); this.groupBox1.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.groupBox1.Size = new System.Drawing.Size(902, 334);
this.groupBox1.TabIndex = 6; this.groupBox1.TabIndex = 6;
this.groupBox1.TabStop = false; this.groupBox1.TabStop = false;
this.groupBox1.Text = "Advanced settings for control freaks"; this.groupBox1.Text = "Advanced settings for control freaks";
// //
// downloadChaptersCbox
//
this.downloadChaptersCbox.AutoSize = true;
this.downloadChaptersCbox.Location = new System.Drawing.Point(10, 24);
this.downloadChaptersCbox.Name = "downloadChaptersCbox";
this.downloadChaptersCbox.Size = new System.Drawing.Size(224, 19);
this.downloadChaptersCbox.TabIndex = 6;
this.downloadChaptersCbox.Text = "Download chapter titles from Audible";
this.downloadChaptersCbox.UseVisualStyleBackColor = true;
//
// SettingsDialog // SettingsDialog
// //
this.AcceptButton = this.saveBtn; this.AcceptButton = this.saveBtn;
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.CancelButton = this.cancelBtn; this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(800, 363); this.ClientSize = new System.Drawing.Size(933, 442);
this.Controls.Add(this.groupBox1); this.Controls.Add(this.groupBox1);
this.Controls.Add(this.cancelBtn); this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.saveBtn); this.Controls.Add(this.saveBtn);
@ -222,6 +252,7 @@
this.Controls.Add(this.booksLocationTb); this.Controls.Add(this.booksLocationTb);
this.Controls.Add(this.booksLocationLbl); this.Controls.Add(this.booksLocationLbl);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "SettingsDialog"; this.Name = "SettingsDialog";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Edit Settings"; this.Text = "Edit Settings";
@ -231,6 +262,7 @@
this.decryptInProgressGb.ResumeLayout(false); this.decryptInProgressGb.ResumeLayout(false);
this.decryptInProgressGb.PerformLayout(); this.decryptInProgressGb.PerformLayout();
this.groupBox1.ResumeLayout(false); this.groupBox1.ResumeLayout(false);
this.groupBox1.PerformLayout();
this.ResumeLayout(false); this.ResumeLayout(false);
this.PerformLayout(); this.PerformLayout();
@ -252,5 +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;
} }
} }

View File

@ -36,6 +36,8 @@ namespace LibationWinForms.Dialogs
? config.Books ? config.Books
: Path.GetDirectoryName(Exe.FileLocationOnDisk); : Path.GetDirectoryName(Exe.FileLocationOnDisk);
downloadChaptersCbox.Checked = config.DownloadChapters;
switch (config.DownloadsInProgressEnum) switch (config.DownloadsInProgressEnum)
{ {
case "LibationFiles": case "LibationFiles":
@ -71,6 +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.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

@ -1,64 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <root>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<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">
@ -117,4 +57,76 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<metadata name="booksLocationLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="booksLocationTb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="booksLocationSearchBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="booksLocationDescLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="downloadsInProgressGb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</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="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">
<value>True</value>
</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="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">
<value>True</value>
</metadata>
<metadata name="cancelBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="groupBox1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="downloadChaptersCbox.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</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">
<value>True</value>
</metadata>
</root> </root>