diff --git a/AaxDecrypter/AAXChapters.cs b/AaxDecrypter/AAXChapters.cs deleted file mode 100644 index f9770e87..00000000 --- a/AaxDecrypter/AAXChapters.cs +++ /dev/null @@ -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() - .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() - .Where(childnode => childnode.Attributes["key"].Value == "title") - .Select(childnode => childnode.Attributes["value"].Value) - .FirstOrDefault(); - - AddChapter(new Chapter(startTime, endTime, chapterTitle)); - } - } - } -} diff --git a/AaxDecrypter/AaxDecrypter.csproj b/AaxDecrypter/AaxDecrypter.csproj index daab451a..a3eeee41 100644 --- a/AaxDecrypter/AaxDecrypter.csproj +++ b/AaxDecrypter/AaxDecrypter.csproj @@ -15,9 +15,6 @@ - - Always - Always @@ -39,9 +36,6 @@ Always - - Always - Always diff --git a/AaxDecrypter/AaxToM4bConverter.cs b/AaxDecrypter/AaxToM4bConverter.cs deleted file mode 100644 index 54000c16..00000000 --- a/AaxDecrypter/AaxToM4bConverter.cs +++ /dev/null @@ -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 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(); - } - /// full c# app. integrated logging. no UI - public class AaxToM4bConverter : IAdvancedAaxToM4bConverter - { - public event EventHandler 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 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 | - * 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 | - * 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 | - * 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 | - * 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 | - * 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; - } - } -} diff --git a/AaxDecrypter/AaxcDownloadConverter.cs b/AaxDecrypter/AaxcDownloadConverter.cs new file mode 100644 index 00000000..cb37d542 --- /dev/null +++ b/AaxDecrypter/AaxcDownloadConverter.cs @@ -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 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 DecryptProgressUpdate; + public event EventHandler 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 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); + } + + /// + /// Calculates the average processing rate based on the last 2 to samples. + /// + /// Position in the audio file last processed + /// The average processing rate, in book_duration_seconds / second. + 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 streamPositions = new Queue(); + + /// + /// Copy all aacx metadata to m4b file, including cover art. + /// + 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().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(); + } + } +} diff --git a/AaxDecrypter/Chapter.cs b/AaxDecrypter/Chapter.cs deleted file mode 100644 index b4507d43..00000000 --- a/AaxDecrypter/Chapter.cs +++ /dev/null @@ -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; - } - /// - /// Chapter start time, in seconds. - /// - public double StartTime { get; private set; } - /// - /// Chapter end time, in seconds. - /// - public double EndTime { get; private set; } - public string Title { get; private set; } - } -} diff --git a/AaxDecrypter/Chapters.cs b/AaxDecrypter/Chapters.cs index 29ec9102..83d0f117 100644 --- a/AaxDecrypter/Chapters.cs +++ b/AaxDecrypter/Chapters.cs @@ -1,40 +1,91 @@ -using System; +using Dinah.Core; +using Dinah.Core.Diagnostics; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Text; namespace AaxDecrypter { - public abstract class Chapters + public class ChapterInfo { - private List _chapterList = new(); + private List _chapterList = new List(); + public IEnumerable Chapters => _chapterList.AsEnumerable(); public int Count => _chapterList.Count; - public Chapter FirstChapter => _chapterList[0]; - public Chapter LastChapter => _chapterList[Count - 1]; - public IEnumerable ChapterList => _chapterList.AsEnumerable(); - public IEnumerable GetBeginningTimes() => ChapterList.Select(c => TimeSpan.FromSeconds(c.StartTime)); - protected void AddChapter(Chapter chapter) + + public ChapterInfo() { } + public ChapterInfo(string audiobookFile) { + 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() + .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() + .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); } - protected void AddChapters(IEnumerable chapters) + public string ToFFMeta(bool includeFFMetaHeader) { - _chapterList.AddRange(chapters); - } - public string GenerateFfmpegChapters() - { - var stringBuilder = new StringBuilder(); + var ffmetaChapters = new StringBuilder(); - foreach (Chapter c in ChapterList) + if (includeFFMetaHeader) + ffmetaChapters.AppendLine(";FFMETADATA1\n"); + + foreach (var c in Chapters) { - stringBuilder.Append("[CHAPTER]\n"); - stringBuilder.Append("TIMEBASE=1/1000\n"); - stringBuilder.Append("START=" + c.StartTime * 1000 + "\n"); - stringBuilder.Append("END=" + c.EndTime * 1000 + "\n"); - stringBuilder.Append("title=" + c.Title + "\n"); + ffmetaChapters.AppendLine(c.ToFFMeta()); } + return ffmetaChapters.ToString(); + } + } + 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); - return stringBuilder.ToString(); + Title = title; + StartOffsetMs = startOffsetMs; + EndOffsetMs = StartOffsetMs + lengthMs; + } + + public string ToFFMeta() + { + return "[CHAPTER]\n" + + "TIMEBASE=1/1000\n" + + "START=" + StartOffsetMs + "\n" + + "END=" + EndOffsetMs + "\n" + + "title=" + Title; } } } diff --git a/AaxDecrypter/Cue.cs b/AaxDecrypter/Cue.cs index 86ec88c3..cc892aa2 100644 --- a/AaxDecrypter/Cue.cs +++ b/AaxDecrypter/Cue.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using System.Text; using Dinah.Core; @@ -8,17 +7,17 @@ namespace AaxDecrypter { public static class Cue { - public static string CreateContents(string filePath, Chapters chapters) + public static string CreateContents(string filePath, ChapterInfo chapters) { var stringBuilder = new StringBuilder(); stringBuilder.AppendLine(GetFileLine(filePath, "MP3")); var trackCount = 0; - foreach (Chapter c in chapters.ChapterList) + foreach (var c in chapters.Chapters) { trackCount++; - var startTime = TimeSpan.FromSeconds(c.StartTime); + var startTime = TimeSpan.FromMilliseconds(c.StartOffsetMs); stringBuilder.AppendLine($"TRACK {trackCount} AUDIO"); stringBuilder.AppendLine($" TITLE \"{c.Title}\""); diff --git a/AaxDecrypter/DecryptLib/AtomicParsley.exe b/AaxDecrypter/DecryptLib/AtomicParsley.exe deleted file mode 100644 index fd840692..00000000 Binary files a/AaxDecrypter/DecryptLib/AtomicParsley.exe and /dev/null differ diff --git a/AaxDecrypter/DecryptLib/postproc-54.dll b/AaxDecrypter/DecryptLib/postproc-54.dll deleted file mode 100644 index 5ec9448e..00000000 Binary files a/AaxDecrypter/DecryptLib/postproc-54.dll and /dev/null differ diff --git a/AaxDecrypter/DecryptSupportLibraries.cs b/AaxDecrypter/DecryptSupportLibraries.cs index 7053e9ed..8eb5ddb9 100644 --- a/AaxDecrypter/DecryptSupportLibraries.cs +++ b/AaxDecrypter/DecryptSupportLibraries.cs @@ -6,13 +6,11 @@ namespace AaxDecrypter { // OTHER EXTERNAL DEPENDENCIES // 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 decryptLib_ { get; } = Path.Combine(appPath_, "DecryptLib"); - public static string ffmpegPath { get; } = Path.Combine(decryptLib_, "ffmpeg.exe"); public static string ffprobePath { get; } = Path.Combine(decryptLib_, "ffprobe.exe"); - public static string atomicParsleyPath { get; } = Path.Combine(decryptLib_, "AtomicParsley.exe"); } -} +} \ No newline at end of file diff --git a/AaxDecrypter/DownloadLicense.cs b/AaxDecrypter/DownloadLicense.cs new file mode 100644 index 00000000..d75b4192 --- /dev/null +++ b/AaxDecrypter/DownloadLicense.cs @@ -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; + } + } +} diff --git a/AaxDecrypter/EncodingInfo.cs b/AaxDecrypter/EncodingInfo.cs deleted file mode 100644 index d09a0fb1..00000000 --- a/AaxDecrypter/EncodingInfo.cs +++ /dev/null @@ -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; - } - } - } - } -} diff --git a/AaxDecrypter/FFMpegAaxcProcesser.cs b/AaxDecrypter/FFMpegAaxcProcesser.cs new file mode 100644 index 00000000..89734dcf --- /dev/null +++ b/AaxDecrypter/FFMpegAaxcProcesser.cs @@ -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 +{ + + /// + /// Download audible aaxc, decrypt, and remux with chapters. + /// + class FFMpegAaxcProcesser + { + public event EventHandler 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; + } + } +} \ No newline at end of file diff --git a/AaxDecrypter/NFO.cs b/AaxDecrypter/NFO.cs index 906cee38..ab652f45 100644 --- a/AaxDecrypter/NFO.cs +++ b/AaxDecrypter/NFO.cs @@ -1,29 +1,34 @@ -namespace AaxDecrypter + +namespace AaxDecrypter { 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 = (_hours > 0 ? _hours + " hours, " : "") - + tags.duration.Minutes + " minutes, " - + tags.duration.Seconds + " seconds"; + + aaxcTagLib.Properties.Duration.Minutes + " minutes, " + + aaxcTagLib.Properties.Duration.Seconds + " seconds"; var header = "General Information\r\n" + "===================\r\n" - + $" Title: {tags.title}\r\n" - + $" Author: {tags.author}\r\n" - + $" Read By: {tags.narrator}\r\n" - + $" Copyright: {tags.year}\r\n" - + $" Audiobook Copyright: {tags.year}\r\n"; - if (tags.genre != "") - header += $" Genre: {tags.genre}\r\n"; + + $" Title: {aaxcTagLib.Tag.Title.Replace(" (Unabridged)", "")}\r\n" + + $" Author: {aaxcTagLib.Tag.FirstPerformer ?? "[unknown]"}\r\n" + + $" Read By: {aaxcTagLib.GetTag(TagLib.TagTypes.Apple).Narrator??"[unknown]"}\r\n" + + $" Copyright: {aaxcTagLib.Tag.Year}\r\n" + + $" Audiobook Copyright: {aaxcTagLib.Tag.Year}\r\n"; + if (!string.IsNullOrEmpty(aaxcTagLib.Tag.FirstGenre)) + header += $" Genre: {aaxcTagLib.Tag.FirstGenre}\r\n"; var s = header - + $" Publisher: {tags.publisher}\r\n" + + $" Publisher: {tag.Publisher ?? ""}\r\n" + $" Duration: {myDuration}\r\n" + $" Chapters: {chapters.Count}\r\n" + "\r\n" @@ -31,22 +36,22 @@ + "Media Information\r\n" + "=================\r\n" + " Source Format: Audible AAX\r\n" - + $" Source Sample Rate: {encodingInfo.sampleRate} Hz\r\n" - + $" Source Channels: {encodingInfo.channels}\r\n" - + $" Source Bitrate: {encodingInfo.originalBitrate} kbits\r\n" + + $" Source Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n" + + $" Source Channels: {aaxcTagLib.Properties.AudioChannels}\r\n" + + $" Source Bitrate: {aaxcTagLib.Properties.AudioBitrate} kbits\r\n" + "\r\n" + " Lossless Encode: Yes\r\n" + " Encoded Codec: AAC / M4B\r\n" - + $" Encoded Sample Rate: {encodingInfo.sampleRate} Hz\r\n" - + $" Encoded Channels: {encodingInfo.channels}\r\n" - + $" Encoded Bitrate: {encodingInfo.originalBitrate} kbits\r\n" + + $" Encoded Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n" + + $" Encoded Channels: {aaxcTagLib.Properties.AudioChannels}\r\n" + + $" Encoded Bitrate: {aaxcTagLib.Properties.AudioBitrate} kbits\r\n" + "\r\n" + $" Ripper: {ripper}\r\n" + "\r\n" + "\r\n" + "Book Description\r\n" + "================\r\n" - + tags.comments; + + (!string.IsNullOrWhiteSpace(tag.LongDescription) ? tag.LongDescription : tag.Description); return s; } diff --git a/AaxDecrypter/NetworkFileAbstraction.cs b/AaxDecrypter/NetworkFileAbstraction.cs new file mode 100644 index 00000000..136236ca --- /dev/null +++ b/AaxDecrypter/NetworkFileAbstraction.cs @@ -0,0 +1,135 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace AaxDecrypter +{ + /// + /// Provides a for a file over Http. + /// + class NetworkFileAbstraction : TagLib.File.IFileAbstraction + { + private NetworkFileStream aaxNetworkStream; + + public static async Task 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(); + } + /// + /// Read more data from into as needed. + /// + /// Length of strem required for the operation. + 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; + } + } + } +} diff --git a/AaxDecrypter/Tags.cs b/AaxDecrypter/Tags.cs deleted file mode 100644 index 25182600..00000000 --- a/AaxDecrypter/Tags.cs +++ /dev/null @@ -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"; - } -} diff --git a/FileLiberator/AaxcDownloadDecrypt/DownloadBookDummy.cs b/FileLiberator/AaxcDownloadDecrypt/DownloadBookDummy.cs new file mode 100644 index 00000000..6ebc8329 --- /dev/null +++ b/FileLiberator/AaxcDownloadDecrypt/DownloadBookDummy.cs @@ -0,0 +1,16 @@ +using DataLayer; +using Dinah.Core.ErrorHandling; +using System.Threading.Tasks; + +namespace FileLiberator.AaxcDownloadDecrypt +{ + public class DownloadBookDummy : DownloadableBase + { + public override Task ProcessItemAsync(LibraryBook libraryBook) => Task.FromResult(new StatusHandler()); + + public override bool Validate(LibraryBook libraryBook) + { + return true; + } + } +} diff --git a/FileLiberator/AaxcDownloadDecrypt/DownloadDecryptBook.cs b/FileLiberator/AaxcDownloadDecrypt/DownloadDecryptBook.cs new file mode 100644 index 00000000..4ae53569 --- /dev/null +++ b/FileLiberator/AaxcDownloadDecrypt/DownloadDecryptBook.cs @@ -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 Begin; + public event EventHandler DecryptBegin; + public event EventHandler TitleDiscovered; + public event EventHandler AuthorsDiscovered; + public event EventHandler NarratorsDiscovered; + public event EventHandler CoverImageFilepathDiscovered; + public event EventHandler UpdateProgress; + public event EventHandler UpdateRemainingTime; + public event EventHandler DecryptCompleted; + public event EventHandler Completed; + public event EventHandler StatusUpdate; + + private AaxcDownloadConverter aaxcDownloader; + public async Task 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 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 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(); + } + } +} diff --git a/FileLiberator/BackupBook.cs b/FileLiberator/BackupBook.cs index 72ff0dd3..604994c4 100644 --- a/FileLiberator/BackupBook.cs +++ b/FileLiberator/BackupBook.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using DataLayer; using Dinah.Core.ErrorHandling; +using FileLiberator.AaxcDownloadDecrypt; using FileManager; namespace FileLiberator @@ -21,8 +22,9 @@ namespace FileLiberator public event EventHandler StatusUpdate; public event EventHandler 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 bool Validate(LibraryBook libraryBook) diff --git a/FileLiberator/DecryptBook.cs b/FileLiberator/DecryptBook.cs deleted file mode 100644 index 47ef3e3a..00000000 --- a/FileLiberator/DecryptBook.cs +++ /dev/null @@ -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 -{ - /// - /// 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 - /// - public class DecryptBook : IDecryptable - { - public event EventHandler Begin; - public event EventHandler StatusUpdate; - public event EventHandler DecryptBegin; - - public event EventHandler TitleDiscovered; - public event EventHandler AuthorsDiscovered; - public event EventHandler NarratorsDiscovered; - public event EventHandler CoverImageFilepathDiscovered; - public event EventHandler UpdateProgress; - - public event EventHandler DecryptCompleted; - public event EventHandler 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 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 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 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(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 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; - } - } -} diff --git a/FileLiberator/DownloadBook.cs b/FileLiberator/DownloadBook.cs deleted file mode 100644 index c32aa51e..00000000 --- a/FileLiberator/DownloadBook.cs +++ /dev/null @@ -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 -{ - /// - /// 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 - /// - 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 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 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(); - } -} diff --git a/FileLiberator/DownloadedChapters.cs b/FileLiberator/DownloadedChapters.cs deleted file mode 100644 index 7a95f83c..00000000 --- a/FileLiberator/DownloadedChapters.cs +++ /dev/null @@ -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))); - } - } -} diff --git a/FileLiberator/IDecryptable.cs b/FileLiberator/IDecryptable.cs index 14a75812..1c90ccfc 100644 --- a/FileLiberator/IDecryptable.cs +++ b/FileLiberator/IDecryptable.cs @@ -11,7 +11,9 @@ namespace FileLiberator event EventHandler NarratorsDiscovered; event EventHandler CoverImageFilepathDiscovered; event EventHandler UpdateProgress; + event EventHandler UpdateRemainingTime; event EventHandler DecryptCompleted; + void Cancel(); } } diff --git a/FileManager/Configuration.cs b/FileManager/Configuration.cs index f970e7c4..2f72616e 100644 --- a/FileManager/Configuration.cs +++ b/FileManager/Configuration.cs @@ -83,13 +83,12 @@ namespace FileManager set => persistentDictionary.Set(nameof(DecryptInProgressEnum), value); } - [Description("Retain .aax files after decrypting?")] - public bool RetainAaxFiles + [Description("Download chapter titles from Audible?")] + public bool DownloadChapters { - get => persistentDictionary.Get(nameof(RetainAaxFiles)); - set => persistentDictionary.Set(nameof(RetainAaxFiles), value); + get => persistentDictionary.Get(nameof(DownloadChapters)); + 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 // singleton stuff diff --git a/LibationLauncher/LibationLauncher.csproj b/LibationLauncher/LibationLauncher.csproj index 3b7129f2..3d4757c8 100644 --- a/LibationLauncher/LibationLauncher.csproj +++ b/LibationLauncher/LibationLauncher.csproj @@ -13,7 +13,7 @@ win-x64 - 4.4.0.64 + 4.4.0.181 diff --git a/LibationLauncher/Program.cs b/LibationLauncher/Program.cs index bc001ad6..ea9d6f09 100644 --- a/LibationLauncher/Program.cs +++ b/LibationLauncher/Program.cs @@ -60,6 +60,7 @@ namespace LibationLauncher config.DownloadsInProgressEnum ??= "WinTemp"; config.DecryptInProgressEnum ??= "WinTemp"; config.Books ??= Configuration.AppDir; + config.DownloadChapters = true; }; // setupDialog.BasicBtn_Click += (_, __) => // no action needed 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 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)) return; diff --git a/LibationWinForms/BookLiberation/DecryptForm.Designer.cs b/LibationWinForms/BookLiberation/DecryptForm.Designer.cs index 5f9a6a8c..19572211 100644 --- a/LibationWinForms/BookLiberation/DecryptForm.Designer.cs +++ b/LibationWinForms/BookLiberation/DecryptForm.Designer.cs @@ -32,14 +32,16 @@ this.bookInfoLbl = new System.Windows.Forms.Label(); this.progressBar1 = new System.Windows.Forms.ProgressBar(); this.rtbLog = new System.Windows.Forms.RichTextBox(); + this.remainingTimeLbl = new System.Windows.Forms.Label(); ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); this.SuspendLayout(); // // 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.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.TabIndex = 0; this.pictureBox1.TabStop = false; @@ -47,9 +49,10 @@ // bookInfoLbl // 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.Size = new System.Drawing.Size(100, 13); + this.bookInfoLbl.Size = new System.Drawing.Size(121, 15); this.bookInfoLbl.TabIndex = 0; 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) | 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.Size = new System.Drawing.Size(582, 23); + this.progressBar1.Size = new System.Drawing.Size(611, 27); this.progressBar1.TabIndex = 2; // // rtbLog @@ -67,21 +71,33 @@ 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.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.Size = new System.Drawing.Size(582, 402); + this.rtbLog.Size = new System.Drawing.Size(678, 463); this.rtbLog.TabIndex = 1; 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 // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); 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.progressBar1); this.Controls.Add(this.bookInfoLbl); this.Controls.Add(this.pictureBox1); + this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.Name = "DecryptForm"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; this.Text = "DecryptForm"; @@ -99,5 +115,6 @@ private System.Windows.Forms.Label bookInfoLbl; private System.Windows.Forms.ProgressBar progressBar1; private System.Windows.Forms.RichTextBox rtbLog; + private System.Windows.Forms.Label remainingTimeLbl; } } \ No newline at end of file diff --git a/LibationWinForms/BookLiberation/DecryptForm.cs b/LibationWinForms/BookLiberation/DecryptForm.cs index 275a7f07..1f76bafd 100644 --- a/LibationWinForms/BookLiberation/DecryptForm.cs +++ b/LibationWinForms/BookLiberation/DecryptForm.cs @@ -59,6 +59,17 @@ namespace LibationWinForms.BookLiberation public void SetCoverImage(byte[] 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"); + } } } diff --git a/LibationWinForms/BookLiberation/DecryptForm.resx b/LibationWinForms/BookLiberation/DecryptForm.resx index 1af7de15..f298a7be 100644 --- a/LibationWinForms/BookLiberation/DecryptForm.resx +++ b/LibationWinForms/BookLiberation/DecryptForm.resx @@ -1,64 +1,4 @@ - - - + diff --git a/LibationWinForms/BookLiberation/ProcessorAutomationController.cs b/LibationWinForms/BookLiberation/ProcessorAutomationController.cs index 2b350866..6efc3550 100644 --- a/LibationWinForms/BookLiberation/ProcessorAutomationController.cs +++ b/LibationWinForms/BookLiberation/ProcessorAutomationController.cs @@ -264,6 +264,7 @@ namespace LibationWinForms.BookLiberation void narratorsDiscovered(object _, string narrators) => decryptDialog.SetNarratorNames(narrators); void coverImageFilepathDiscovered(object _, byte[] coverBytes) => decryptDialog.SetCoverImage(coverBytes); void updateProgress(object _, int percentage) => decryptDialog.UpdateProgress(percentage); + void updateRemainingTime(object _, TimeSpan remaining) => decryptDialog.UpdateRemainingTime(remaining); void decryptCompleted(object _, string __) => decryptDialog.Close(); #endregion @@ -276,6 +277,7 @@ namespace LibationWinForms.BookLiberation decryptBook.NarratorsDiscovered += narratorsDiscovered; decryptBook.CoverImageFilepathDiscovered += coverImageFilepathDiscovered; decryptBook.UpdateProgress += updateProgress; + decryptBook.UpdateRemainingTime += updateRemainingTime; decryptBook.DecryptCompleted += decryptCompleted; #endregion @@ -291,8 +293,10 @@ namespace LibationWinForms.BookLiberation decryptBook.NarratorsDiscovered -= narratorsDiscovered; decryptBook.CoverImageFilepathDiscovered -= coverImageFilepathDiscovered; decryptBook.UpdateProgress -= updateProgress; + decryptBook.UpdateRemainingTime -= updateRemainingTime; decryptBook.DecryptCompleted -= decryptCompleted; + decryptBook.Cancel(); }; #endregion } diff --git a/LibationWinForms/Dialogs/SettingsDialog.Designer.cs b/LibationWinForms/Dialogs/SettingsDialog.Designer.cs index dbab722f..76d1791b 100644 --- a/LibationWinForms/Dialogs/SettingsDialog.Designer.cs +++ b/LibationWinForms/Dialogs/SettingsDialog.Designer.cs @@ -28,211 +28,243 @@ /// private void InitializeComponent() { - this.booksLocationLbl = new System.Windows.Forms.Label(); - this.booksLocationTb = new System.Windows.Forms.TextBox(); - this.booksLocationSearchBtn = new System.Windows.Forms.Button(); - this.booksLocationDescLbl = new System.Windows.Forms.Label(); - this.downloadsInProgressGb = new System.Windows.Forms.GroupBox(); - this.downloadsInProgressLibationFilesRb = new System.Windows.Forms.RadioButton(); - this.downloadsInProgressWinTempRb = new System.Windows.Forms.RadioButton(); - this.downloadsInProgressDescLbl = new System.Windows.Forms.Label(); - this.decryptInProgressGb = new System.Windows.Forms.GroupBox(); - this.decryptInProgressLibationFilesRb = new System.Windows.Forms.RadioButton(); - this.decryptInProgressWinTempRb = new System.Windows.Forms.RadioButton(); - this.decryptInProgressDescLbl = new System.Windows.Forms.Label(); - this.saveBtn = new System.Windows.Forms.Button(); - this.cancelBtn = new System.Windows.Forms.Button(); - this.groupBox1 = new System.Windows.Forms.GroupBox(); - this.downloadsInProgressGb.SuspendLayout(); - this.decryptInProgressGb.SuspendLayout(); - this.groupBox1.SuspendLayout(); - this.SuspendLayout(); - // - // booksLocationLbl - // - this.booksLocationLbl.AutoSize = true; - this.booksLocationLbl.Location = new System.Drawing.Point(12, 17); - this.booksLocationLbl.Name = "booksLocationLbl"; - this.booksLocationLbl.Size = new System.Drawing.Size(77, 13); - this.booksLocationLbl.TabIndex = 0; - this.booksLocationLbl.Text = "Books location"; - // - // booksLocationTb - // - this.booksLocationTb.Location = new System.Drawing.Point(95, 14); - this.booksLocationTb.Name = "booksLocationTb"; - this.booksLocationTb.Size = new System.Drawing.Size(652, 20); - this.booksLocationTb.TabIndex = 1; - // - // booksLocationSearchBtn - // - this.booksLocationSearchBtn.Location = new System.Drawing.Point(753, 12); - this.booksLocationSearchBtn.Name = "booksLocationSearchBtn"; - this.booksLocationSearchBtn.Size = new System.Drawing.Size(35, 23); - this.booksLocationSearchBtn.TabIndex = 2; - this.booksLocationSearchBtn.Text = "..."; - this.booksLocationSearchBtn.UseVisualStyleBackColor = true; - this.booksLocationSearchBtn.Click += new System.EventHandler(this.booksLocationSearchBtn_Click); - // - // booksLocationDescLbl - // - this.booksLocationDescLbl.AutoSize = true; - this.booksLocationDescLbl.Location = new System.Drawing.Point(92, 37); - this.booksLocationDescLbl.Name = "booksLocationDescLbl"; - this.booksLocationDescLbl.Size = new System.Drawing.Size(36, 13); - this.booksLocationDescLbl.TabIndex = 3; - this.booksLocationDescLbl.Text = "[desc]"; - // - // downloadsInProgressGb - // - this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressLibationFilesRb); - this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressWinTempRb); - this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressDescLbl); - this.downloadsInProgressGb.Location = new System.Drawing.Point(15, 19); - this.downloadsInProgressGb.Name = "downloadsInProgressGb"; - this.downloadsInProgressGb.Size = new System.Drawing.Size(758, 117); - this.downloadsInProgressGb.TabIndex = 4; - this.downloadsInProgressGb.TabStop = false; - this.downloadsInProgressGb.Text = "Downloads in progress"; - // - // downloadsInProgressLibationFilesRb - // - this.downloadsInProgressLibationFilesRb.AutoSize = true; - this.downloadsInProgressLibationFilesRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft; - this.downloadsInProgressLibationFilesRb.Location = new System.Drawing.Point(9, 81); - this.downloadsInProgressLibationFilesRb.Name = "downloadsInProgressLibationFilesRb"; - this.downloadsInProgressLibationFilesRb.Size = new System.Drawing.Size(193, 30); - this.downloadsInProgressLibationFilesRb.TabIndex = 2; - this.downloadsInProgressLibationFilesRb.TabStop = true; - this.downloadsInProgressLibationFilesRb.Text = "[desc]\r\n[libationFiles\\DownloadsInProgress]"; - this.downloadsInProgressLibationFilesRb.UseVisualStyleBackColor = true; - // - // downloadsInProgressWinTempRb - // - this.downloadsInProgressWinTempRb.AutoSize = true; - this.downloadsInProgressWinTempRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft; - this.downloadsInProgressWinTempRb.Location = new System.Drawing.Point(9, 45); - this.downloadsInProgressWinTempRb.Name = "downloadsInProgressWinTempRb"; - this.downloadsInProgressWinTempRb.Size = new System.Drawing.Size(182, 30); - this.downloadsInProgressWinTempRb.TabIndex = 1; - this.downloadsInProgressWinTempRb.TabStop = true; - this.downloadsInProgressWinTempRb.Text = "[desc]\r\n[winTemp\\DownloadsInProgress]"; - this.downloadsInProgressWinTempRb.UseVisualStyleBackColor = true; - // - // downloadsInProgressDescLbl - // - this.downloadsInProgressDescLbl.AutoSize = true; - this.downloadsInProgressDescLbl.Location = new System.Drawing.Point(6, 16); - this.downloadsInProgressDescLbl.Name = "downloadsInProgressDescLbl"; - this.downloadsInProgressDescLbl.Size = new System.Drawing.Size(38, 26); - this.downloadsInProgressDescLbl.TabIndex = 0; - this.downloadsInProgressDescLbl.Text = "[desc]\r\n[line 2]"; - // - // decryptInProgressGb - // - this.decryptInProgressGb.Controls.Add(this.decryptInProgressLibationFilesRb); - this.decryptInProgressGb.Controls.Add(this.decryptInProgressWinTempRb); - this.decryptInProgressGb.Controls.Add(this.decryptInProgressDescLbl); - this.decryptInProgressGb.Location = new System.Drawing.Point(9, 144); - this.decryptInProgressGb.Name = "decryptInProgressGb"; - this.decryptInProgressGb.Size = new System.Drawing.Size(758, 117); - this.decryptInProgressGb.TabIndex = 5; - this.decryptInProgressGb.TabStop = false; - this.decryptInProgressGb.Text = "Decrypt in progress"; - // - // decryptInProgressLibationFilesRb - // - this.decryptInProgressLibationFilesRb.AutoSize = true; - this.decryptInProgressLibationFilesRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft; - this.decryptInProgressLibationFilesRb.Location = new System.Drawing.Point(6, 81); - this.decryptInProgressLibationFilesRb.Name = "decryptInProgressLibationFilesRb"; - this.decryptInProgressLibationFilesRb.Size = new System.Drawing.Size(177, 30); - this.decryptInProgressLibationFilesRb.TabIndex = 2; - this.decryptInProgressLibationFilesRb.TabStop = true; - this.decryptInProgressLibationFilesRb.Text = "[desc]\r\n[libationFiles\\DecryptInProgress]"; - this.decryptInProgressLibationFilesRb.UseVisualStyleBackColor = true; - // - // decryptInProgressWinTempRb - // - this.decryptInProgressWinTempRb.AutoSize = true; - this.decryptInProgressWinTempRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft; - this.decryptInProgressWinTempRb.Location = new System.Drawing.Point(6, 45); - this.decryptInProgressWinTempRb.Name = "decryptInProgressWinTempRb"; - this.decryptInProgressWinTempRb.Size = new System.Drawing.Size(166, 30); - this.decryptInProgressWinTempRb.TabIndex = 1; - this.decryptInProgressWinTempRb.TabStop = true; - this.decryptInProgressWinTempRb.Text = "[desc]\r\n[winTemp\\DecryptInProgress]"; - this.decryptInProgressWinTempRb.UseVisualStyleBackColor = true; - // - // decryptInProgressDescLbl - // - this.decryptInProgressDescLbl.AutoSize = true; - this.decryptInProgressDescLbl.Location = new System.Drawing.Point(6, 16); - this.decryptInProgressDescLbl.Name = "decryptInProgressDescLbl"; - this.decryptInProgressDescLbl.Size = new System.Drawing.Size(38, 26); - this.decryptInProgressDescLbl.TabIndex = 0; - this.decryptInProgressDescLbl.Text = "[desc]\r\n[line 2]"; - // - // saveBtn - // - 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.Name = "saveBtn"; - this.saveBtn.Size = new System.Drawing.Size(75, 23); - this.saveBtn.TabIndex = 7; - this.saveBtn.Text = "Save"; - this.saveBtn.UseVisualStyleBackColor = true; - this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click); - // - // cancelBtn - // - 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.Location = new System.Drawing.Point(713, 328); - this.cancelBtn.Name = "cancelBtn"; - this.cancelBtn.Size = new System.Drawing.Size(75, 23); - this.cancelBtn.TabIndex = 8; - this.cancelBtn.Text = "Cancel"; - this.cancelBtn.UseVisualStyleBackColor = true; - this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click); - // - // groupBox1 - // - this.groupBox1.Controls.Add(this.downloadsInProgressGb); - this.groupBox1.Controls.Add(this.decryptInProgressGb); - this.groupBox1.Location = new System.Drawing.Point(15, 53); - this.groupBox1.Name = "groupBox1"; - this.groupBox1.Size = new System.Drawing.Size(773, 269); - this.groupBox1.TabIndex = 6; - this.groupBox1.TabStop = false; - this.groupBox1.Text = "Advanced settings for control freaks"; - // - // SettingsDialog - // - this.AcceptButton = this.saveBtn; - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.CancelButton = this.cancelBtn; - this.ClientSize = new System.Drawing.Size(800, 363); - this.Controls.Add(this.groupBox1); - this.Controls.Add(this.cancelBtn); - this.Controls.Add(this.saveBtn); - this.Controls.Add(this.booksLocationDescLbl); - this.Controls.Add(this.booksLocationSearchBtn); - this.Controls.Add(this.booksLocationTb); - this.Controls.Add(this.booksLocationLbl); - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; - this.Name = "SettingsDialog"; - this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; - this.Text = "Edit Settings"; - this.Load += new System.EventHandler(this.SettingsDialog_Load); - this.downloadsInProgressGb.ResumeLayout(false); - this.downloadsInProgressGb.PerformLayout(); - this.decryptInProgressGb.ResumeLayout(false); - this.decryptInProgressGb.PerformLayout(); - this.groupBox1.ResumeLayout(false); - this.ResumeLayout(false); - this.PerformLayout(); + this.booksLocationLbl = new System.Windows.Forms.Label(); + this.booksLocationTb = new System.Windows.Forms.TextBox(); + this.booksLocationSearchBtn = new System.Windows.Forms.Button(); + this.booksLocationDescLbl = new System.Windows.Forms.Label(); + this.downloadsInProgressGb = new System.Windows.Forms.GroupBox(); + this.downloadsInProgressLibationFilesRb = new System.Windows.Forms.RadioButton(); + this.downloadsInProgressWinTempRb = new System.Windows.Forms.RadioButton(); + this.downloadsInProgressDescLbl = new System.Windows.Forms.Label(); + this.decryptInProgressGb = new System.Windows.Forms.GroupBox(); + this.decryptInProgressLibationFilesRb = new System.Windows.Forms.RadioButton(); + this.decryptInProgressWinTempRb = new System.Windows.Forms.RadioButton(); + this.decryptInProgressDescLbl = new System.Windows.Forms.Label(); + this.saveBtn = new System.Windows.Forms.Button(); + this.cancelBtn = new System.Windows.Forms.Button(); + this.groupBox1 = new System.Windows.Forms.GroupBox(); + this.downloadChaptersCbox = new System.Windows.Forms.CheckBox(); + this.downloadsInProgressGb.SuspendLayout(); + this.decryptInProgressGb.SuspendLayout(); + this.groupBox1.SuspendLayout(); + this.SuspendLayout(); + // + // booksLocationLbl + // + this.booksLocationLbl.AutoSize = true; + 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.Size = new System.Drawing.Size(85, 15); + this.booksLocationLbl.TabIndex = 0; + this.booksLocationLbl.Text = "Books location"; + // + // booksLocationTb + // + 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.Size = new System.Drawing.Size(760, 23); + this.booksLocationTb.TabIndex = 1; + // + // booksLocationSearchBtn + // + 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.Size = new System.Drawing.Size(41, 27); + this.booksLocationSearchBtn.TabIndex = 2; + this.booksLocationSearchBtn.Text = "..."; + this.booksLocationSearchBtn.UseVisualStyleBackColor = true; + this.booksLocationSearchBtn.Click += new System.EventHandler(this.booksLocationSearchBtn_Click); + // + // booksLocationDescLbl + // + this.booksLocationDescLbl.AutoSize = true; + 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.Size = new System.Drawing.Size(39, 15); + this.booksLocationDescLbl.TabIndex = 3; + this.booksLocationDescLbl.Text = "[desc]"; + // + // downloadsInProgressGb + // + this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressLibationFilesRb); + this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressWinTempRb); + this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressDescLbl); + 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.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.TabStop = false; + this.downloadsInProgressGb.Text = "Downloads in progress"; + // + // downloadsInProgressLibationFilesRb + // + this.downloadsInProgressLibationFilesRb.AutoSize = true; + this.downloadsInProgressLibationFilesRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft; + 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.Size = new System.Drawing.Size(215, 34); + this.downloadsInProgressLibationFilesRb.TabIndex = 2; + this.downloadsInProgressLibationFilesRb.TabStop = true; + this.downloadsInProgressLibationFilesRb.Text = "[desc]\r\n[libationFiles\\DownloadsInProgress]"; + this.downloadsInProgressLibationFilesRb.UseVisualStyleBackColor = true; + // + // downloadsInProgressWinTempRb + // + this.downloadsInProgressWinTempRb.AutoSize = true; + this.downloadsInProgressWinTempRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft; + 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.Size = new System.Drawing.Size(200, 34); + this.downloadsInProgressWinTempRb.TabIndex = 1; + this.downloadsInProgressWinTempRb.TabStop = true; + this.downloadsInProgressWinTempRb.Text = "[desc]\r\n[winTemp\\DownloadsInProgress]"; + this.downloadsInProgressWinTempRb.UseVisualStyleBackColor = true; + // + // downloadsInProgressDescLbl + // + this.downloadsInProgressDescLbl.AutoSize = true; + 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.Size = new System.Drawing.Size(43, 30); + this.downloadsInProgressDescLbl.TabIndex = 0; + this.downloadsInProgressDescLbl.Text = "[desc]\r\n[line 2]"; + // + // decryptInProgressGb + // + this.decryptInProgressGb.Controls.Add(this.decryptInProgressLibationFilesRb); + this.decryptInProgressGb.Controls.Add(this.decryptInProgressWinTempRb); + this.decryptInProgressGb.Controls.Add(this.decryptInProgressDescLbl); + 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.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.TabStop = false; + this.decryptInProgressGb.Text = "Decrypt in progress"; + // + // decryptInProgressLibationFilesRb + // + this.decryptInProgressLibationFilesRb.AutoSize = true; + this.decryptInProgressLibationFilesRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft; + 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.Size = new System.Drawing.Size(197, 34); + this.decryptInProgressLibationFilesRb.TabIndex = 2; + this.decryptInProgressLibationFilesRb.TabStop = true; + this.decryptInProgressLibationFilesRb.Text = "[desc]\r\n[libationFiles\\DecryptInProgress]"; + this.decryptInProgressLibationFilesRb.UseVisualStyleBackColor = true; + // + // decryptInProgressWinTempRb + // + this.decryptInProgressWinTempRb.AutoSize = true; + this.decryptInProgressWinTempRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft; + 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.Size = new System.Drawing.Size(182, 34); + this.decryptInProgressWinTempRb.TabIndex = 1; + this.decryptInProgressWinTempRb.TabStop = true; + this.decryptInProgressWinTempRb.Text = "[desc]\r\n[winTemp\\DecryptInProgress]"; + this.decryptInProgressWinTempRb.UseVisualStyleBackColor = true; + // + // decryptInProgressDescLbl + // + this.decryptInProgressDescLbl.AutoSize = true; + 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.Size = new System.Drawing.Size(43, 30); + this.decryptInProgressDescLbl.TabIndex = 0; + this.decryptInProgressDescLbl.Text = "[desc]\r\n[line 2]"; + // + // saveBtn + // + this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.saveBtn.Location = new System.Drawing.Point(714, 401); + this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + this.saveBtn.Name = "saveBtn"; + this.saveBtn.Size = new System.Drawing.Size(88, 27); + this.saveBtn.TabIndex = 7; + this.saveBtn.Text = "Save"; + this.saveBtn.UseVisualStyleBackColor = true; + this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click); + // + // cancelBtn + // + 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.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.Size = new System.Drawing.Size(88, 27); + this.cancelBtn.TabIndex = 8; + this.cancelBtn.Text = "Cancel"; + this.cancelBtn.UseVisualStyleBackColor = true; + this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click); + // + // groupBox1 + // + this.groupBox1.Controls.Add(this.downloadChaptersCbox); + this.groupBox1.Controls.Add(this.downloadsInProgressGb); + this.groupBox1.Controls.Add(this.decryptInProgressGb); + 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.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.TabStop = false; + 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 + // + this.AcceptButton = this.saveBtn; + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.cancelBtn; + this.ClientSize = new System.Drawing.Size(933, 442); + this.Controls.Add(this.groupBox1); + this.Controls.Add(this.cancelBtn); + this.Controls.Add(this.saveBtn); + this.Controls.Add(this.booksLocationDescLbl); + this.Controls.Add(this.booksLocationSearchBtn); + this.Controls.Add(this.booksLocationTb); + this.Controls.Add(this.booksLocationLbl); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; + this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + this.Name = "SettingsDialog"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Edit Settings"; + this.Load += new System.EventHandler(this.SettingsDialog_Load); + this.downloadsInProgressGb.ResumeLayout(false); + this.downloadsInProgressGb.PerformLayout(); + this.decryptInProgressGb.ResumeLayout(false); + this.decryptInProgressGb.PerformLayout(); + this.groupBox1.ResumeLayout(false); + this.groupBox1.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); } @@ -252,5 +284,6 @@ private System.Windows.Forms.Button saveBtn; private System.Windows.Forms.Button cancelBtn; private System.Windows.Forms.GroupBox groupBox1; - } + private System.Windows.Forms.CheckBox downloadChaptersCbox; + } } \ No newline at end of file diff --git a/LibationWinForms/Dialogs/SettingsDialog.cs b/LibationWinForms/Dialogs/SettingsDialog.cs index 16dd1fe7..35bbb582 100644 --- a/LibationWinForms/Dialogs/SettingsDialog.cs +++ b/LibationWinForms/Dialogs/SettingsDialog.cs @@ -36,6 +36,8 @@ namespace LibationWinForms.Dialogs ? config.Books : Path.GetDirectoryName(Exe.FileLocationOnDisk); + downloadChaptersCbox.Checked = config.DownloadChapters; + switch (config.DownloadsInProgressEnum) { case "LibationFiles": @@ -71,6 +73,7 @@ namespace LibationWinForms.Dialogs private void saveBtn_Click(object sender, EventArgs e) { + config.DownloadChapters = downloadChaptersCbox.Checked; config.DownloadsInProgressEnum = downloadsInProgressLibationFilesRb.Checked ? "LibationFiles" : "WinTemp"; config.DecryptInProgressEnum = decryptInProgressLibationFilesRb.Checked ? "LibationFiles" : "WinTemp"; diff --git a/LibationWinForms/Dialogs/SettingsDialog.resx b/LibationWinForms/Dialogs/SettingsDialog.resx index 1af7de15..b9d23b7e 100644 --- a/LibationWinForms/Dialogs/SettingsDialog.resx +++ b/LibationWinForms/Dialogs/SettingsDialog.resx @@ -1,64 +1,4 @@ - - - + @@ -117,4 +57,76 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + \ No newline at end of file