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/FileLiberator/AaxcDownloadDecrypt/AaxcDownloadConverter.cs b/AaxDecrypter/AaxcDownloadConverter.cs similarity index 62% rename from FileLiberator/AaxcDownloadDecrypt/AaxcDownloadConverter.cs rename to AaxDecrypter/AaxcDownloadConverter.cs index fb1e413e..e38f350e 100644 --- a/FileLiberator/AaxcDownloadDecrypt/AaxcDownloadConverter.cs +++ b/AaxDecrypter/AaxcDownloadConverter.cs @@ -1,15 +1,14 @@ -using AaxDecrypter; -using AudibleApi; -using AudibleApiDTOs; -using Dinah.Core; +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 FileLiberator.AaxcDownloadDecrypt +namespace AaxDecrypter { public interface ISimpleAaxToM4bConverter2 { @@ -19,7 +18,7 @@ namespace FileLiberator.AaxcDownloadDecrypt string outDir { get; } string outputFileName { get; } ChapterInfo chapters { get; } - Tags tags { get; } + TagLib.Mpeg4.File tags { get; } void SetOutputFilename(string outFileName); } @@ -27,24 +26,28 @@ namespace FileLiberator.AaxcDownloadDecrypt { bool Step1_CreateDir(); bool Step2_DownloadAndCombine(); - bool Step3_InsertCoverArt(); + bool Step3_RestoreMetadata(); bool Step4_CreateCue(); bool Step5_CreateNfo(); - bool Step6_Cleanup(); } - class AaxcDownloadConverter : IAdvancedAaxcToM4bConverter + public class AaxcDownloadConverter : IAdvancedAaxcToM4bConverter { 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 Tags tags { get; private set; } - + public TagLib.Mpeg4.File tags { get; private set; } public event EventHandler DecryptProgressUpdate; + public string Title => tags.Tag.Title.Replace(" (Unabridged)", ""); + public string Author => tags.Tag.FirstPerformer ?? "[unknown]"; + public string Narrator => string.IsNullOrWhiteSpace(tags.Tag.FirstComposer) ? tags.GetTag(TagLib.TagTypes.Apple).Narrator : tags.Tag.FirstComposer; + public byte[] CoverArt => tags.Tag.Pictures.Length > 0 ? tags.Tag.Pictures[0].Data.Data : default; + + + private StepSequence steps { get; } private DownloadLicense downloadLicense { get; set; } - private string coverArtPath => Path.Combine(outDir, Path.GetFileName(outputFileName) + ".jpg"); private string metadataPath => Path.Combine(outDir, Path.GetFileName(outputFileName) + ".ffmeta"); public static async Task CreateAsync(string outDirectory, DownloadLicense dlLic, ChapterInfo chapters) @@ -70,10 +73,9 @@ namespace FileLiberator.AaxcDownloadDecrypt ["Step 1: Create Dir"] = Step1_CreateDir, ["Step 2: Download and Combine Audiobook"] = Step2_DownloadAndCombine, - ["Step 3 Insert Cover Art"] = Step3_InsertCoverArt, - ["Step 4 Create Cue"] = Step4_CreateCue, - ["Step 5 Create Nfo"] = Step5_CreateNfo, - ["Step 6: Cleanup"] = Step6_Cleanup, + ["Step 2: Restore Aaxc Metadata"] = Step3_RestoreMetadata, + ["Step 3 Create Cue"] = Step4_CreateCue, + ["Step 4 Create Nfo"] = Step5_CreateNfo, }; downloadLicense = dlLic; @@ -84,17 +86,16 @@ namespace FileLiberator.AaxcDownloadDecrypt { //Get metadata from the file over http var client = new System.Net.Http.HttpClient(); - client.DefaultRequestHeaders.Add("User-Agent", Resources.UserAgent); + client.DefaultRequestHeaders.Add("User-Agent", downloadLicense.UserAgent); var networkFile = await NetworkFileAbstraction.CreateAsync(client, new Uri(downloadLicense.DownloadUrl)); - var tagLibFile = await Task.Run(()=>TagLib.File.Create(networkFile, "audio/mp4", TagLib.ReadStyle.Average)); - tags = new Tags(tagLibFile); + tags = await Task.Run(() => TagLib.File.Create(networkFile, "audio/mp4", TagLib.ReadStyle.Average) as TagLib.Mpeg4.File); var defaultFilename = Path.Combine( outDir, - PathLib.ToPathSafeString(tags.author), - PathLib.ToPathSafeString(tags.title) + ".m4b" + PathLib.ToPathSafeString(tags.Tag.FirstPerformer??"[unknown]"), + PathLib.ToPathSafeString(tags.Tag.Title.Replace(" (Unabridged)", "")) + ".m4b" ); SetOutputFilename(defaultFilename); @@ -119,7 +120,7 @@ namespace FileLiberator.AaxcDownloadDecrypt return false; } - var speedup = (int)(tags.duration.TotalSeconds / (long)Elapsed.TotalSeconds); + var speedup = (int)(tags.Properties.Duration.TotalSeconds / (long)Elapsed.TotalSeconds); Console.WriteLine("Speedup is " + speedup + "x realtime."); Console.WriteLine("Done"); return true; @@ -135,10 +136,9 @@ namespace FileLiberator.AaxcDownloadDecrypt public bool Step2_DownloadAndCombine() { - var ffmpegTags = tags.GenerateFfmpegTags(); - var ffmpegChapters = GenerateFfmpegChapters(chapters); + var ffmetaHeader = $";FFMETADATA1\n"; - File.WriteAllText(metadataPath, ffmpegTags + ffmpegChapters); + File.WriteAllText(metadataPath, ffmetaHeader + chapters.ToFFMeta()); var aaxcProcesser = new FFMpegAaxcProcesser(DecryptSupportLibraries.ffmpegPath); @@ -146,7 +146,7 @@ namespace FileLiberator.AaxcDownloadDecrypt aaxcProcesser.ProcessBook( downloadLicense.DownloadUrl, - Resources.UserAgent, + downloadLicense.UserAgent, downloadLicense.AudibleKey, downloadLicense.AudibleIV, metadataPath, @@ -155,48 +155,65 @@ namespace FileLiberator.AaxcDownloadDecrypt .GetResult(); DecryptProgressUpdate?.Invoke(this, 0); + + FileExt.SafeDelete(metadataPath); return aaxcProcesser.Succeeded; } private void AaxcProcesser_ProgressUpdate(object sender, TimeSpan e) { - double progressPercent = 100 * e.TotalSeconds / tags.duration.TotalSeconds; + double progressPercent = 100 * e.TotalSeconds / tags.Properties.Duration.TotalSeconds; DecryptProgressUpdate?.Invoke(this, (int)progressPercent); - } - private static string GenerateFfmpegChapters(ChapterInfo chapters) - { - var stringBuilder = new System.Text.StringBuilder(); - - foreach (AudibleApiDTOs.Chapter c in chapters.Chapters) + speedSamples.Enqueue(new DataRate { - stringBuilder.Append("[CHAPTER]\n"); - stringBuilder.Append("TIMEBASE=1/1000\n"); - stringBuilder.Append("START=" + c.StartOffsetMs + "\n"); - stringBuilder.Append("END=" + (c.StartOffsetMs + c.LengthMs) + "\n"); - stringBuilder.Append("title=" + c.Title + "\n"); + SampleTime = DateTime.Now, + ProcessPosition = e + }); + + int sampleNum = 5; + + if (speedSamples.Count < sampleNum) return; + + var oldestSample = speedSamples.Dequeue(); + double harmonicDenom = 0; + foreach (var sample in speedSamples) + { + double inverseRate = (sample.SampleTime - oldestSample.SampleTime).TotalSeconds / (sample.ProcessPosition.TotalSeconds - oldestSample.ProcessPosition.TotalSeconds); + harmonicDenom += inverseRate; + oldestSample = sample; } + double averageRate = (sampleNum - 1) / harmonicDenom; - return stringBuilder.ToString(); } - public bool Step3_InsertCoverArt() + private Queue speedSamples = new Queue(5); + private class DataRate { - File.WriteAllBytes(coverArtPath, tags.coverArt); + public DateTime SampleTime; + public TimeSpan ProcessPosition; + } - var insertCoverArtInfo = new System.Diagnostics.ProcessStartInfo + 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 = tags.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. + foreach (var stag in sourceTag) { - FileName = DecryptSupportLibraries.atomicParsleyPath, - Arguments = "\"" + outputFileName + "\" --encodingTool \"" + AppName + "\" --artwork \"" + coverArtPath + "\" --overWrite" - }; - insertCoverArtInfo.RunHidden(); - - // delete temp file - FileExt.SafeDelete(coverArtPath); - + 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)); @@ -208,11 +225,5 @@ namespace FileLiberator.AaxcDownloadDecrypt File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, tags, chapters)); return true; } - - public bool Step6_Cleanup() - { - FileExt.SafeDelete(metadataPath); - return true; - } } } 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..7c571b94 100644 --- a/AaxDecrypter/Chapters.cs +++ b/AaxDecrypter/Chapters.cs @@ -2,39 +2,48 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading.Tasks; 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 void AddChapter(Chapter chapter) { _chapterList.Add(chapter); } - protected void AddChapters(IEnumerable chapters) + public string ToFFMeta() { - _chapterList.AddRange(chapters); - } - public string GenerateFfmpegChapters() - { - var stringBuilder = new StringBuilder(); - - foreach (Chapter c in ChapterList) + var ffmetaChapters = new StringBuilder(); + 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) + { + Title = title; + StartOffsetMs = startOffsetMs; + EndOffsetMs = StartOffsetMs + lengthMs; + } - return stringBuilder.ToString(); + public string ToFFMeta() + { + return "[CHAPTER]\n" + + "TIMEBASE=1/1000\n" + + "START=" + StartOffsetMs + "\n" + + "END=" + EndOffsetMs + "\n" + + "title=" + Title; } } } 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..61ecf571 --- /dev/null +++ b/AaxDecrypter/DownloadLicense.cs @@ -0,0 +1,24 @@ +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) + { + 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..9f98a9e7 --- /dev/null +++ b/AaxDecrypter/FFMpegAaxcProcesser.cs @@ -0,0 +1,109 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace AaxDecrypter +{ + + /// + /// Download audible aaxc, decrypt, remux,and add metadata. + /// + class FFMpegAaxcProcesser + { + public event EventHandler ProgressUpdate; + public string FFMpegPath { get; } + public bool IsRunning { get; private set; } + public bool Succeeded { get; private set; } + + private static Regex processedTimeRegex = new Regex("time=(\\d{2}):(\\d{2}):(\\d{2}).\\d{2}", RegexOptions.IgnoreCase | RegexOptions.Compiled); + public FFMpegAaxcProcesser(string ffmpegPath) + { + FFMpegPath = ffmpegPath; + } + + public async Task ProcessBook(string aaxcUrl, string userAgent, string audibleKey, string audibleIV, string metadataPath, string outputFile) + { + + //This process gets the aaxc from the url and streams the decrypted + //m4b to the output file. Preserves album art, but replaces metadata. + var downloader = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = FFMpegPath, + RedirectStandardError = true, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = false, + WorkingDirectory = Path.GetDirectoryName(FFMpegPath), + ArgumentList = + { + "-ignore_chapters", //prevents ffmpeg from copying chapter info from aaxc to output file + "true", + "-audible_key", + audibleKey, + "-audible_iv", + audibleIV, + "-user_agent", + userAgent, + "-i", + aaxcUrl, + "-f", + "ffmetadata", + "-i", + metadataPath, + "-map_metadata", + "1", + "-c", //audio codec + "copy", //copy stream + "-f", //force output format: adts + "mp4", + outputFile, //pipe output to standard output + "-y" + } + } + }; + + IsRunning = true; + + downloader.ErrorDataReceived += Remuxer_ErrorDataReceived; + downloader.Start(); + downloader.BeginErrorReadLine(); + + //All the work done here. Copy download standard output into + //remuxer standard input + await downloader.WaitForExitAsync(); + + IsRunning = false; + Succeeded = downloader.ExitCode == 0; + } + private void Remuxer_ErrorDataReceived(object sender, DataReceivedEventArgs e) + { + if (string.IsNullOrEmpty(e.Data)) + return; + + 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")) + { + var process = sender as Process; + process.Kill(); + } + } + + } +} diff --git a/AaxDecrypter/NFO.cs b/AaxDecrypter/NFO.cs index 906cee38..22c09799 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 tags, ChapterInfo chapters) { - var _hours = (int)tags.duration.TotalHours; + var tag = tags.GetTag(TagLib.TagTypes.Apple); + + string narator = string.IsNullOrWhiteSpace(tags.Tag.FirstComposer) ? tag.Narrator : tags.Tag.FirstComposer; + + var _hours = (int)tags.Properties.Duration.TotalHours; var myDuration = (_hours > 0 ? _hours + " hours, " : "") - + tags.duration.Minutes + " minutes, " - + tags.duration.Seconds + " seconds"; + + tags.Properties.Duration.Minutes + " minutes, " + + tags.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: {tags.Tag.Title.Replace(" (Unabridged)", "")}\r\n" + + $" Author: {tags.Tag.FirstPerformer ?? "[unknown]"}\r\n" + + $" Read By: {tags.Tag.FirstPerformer??"[unknown]"}\r\n" + + $" Copyright: {tags.Tag.Year}\r\n" + + $" Audiobook Copyright: {tags.Tag.Year}\r\n"; + if (!string.IsNullOrEmpty(tags.Tag.FirstGenre)) + header += $" Genre: {tags.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: {tags.Properties.AudioSampleRate} Hz\r\n" + + $" Source Channels: {tags.Properties.AudioChannels}\r\n" + + $" Source Bitrate: {tags.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: {tags.Properties.AudioSampleRate} Hz\r\n" + + $" Encoded Channels: {tags.Properties.AudioChannels}\r\n" + + $" Encoded Bitrate: {tags.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/FileLiberator/AaxcDownloadDecrypt/NetworkFileAbstraction.cs b/AaxDecrypter/NetworkFileAbstraction.cs similarity index 99% rename from FileLiberator/AaxcDownloadDecrypt/NetworkFileAbstraction.cs rename to AaxDecrypter/NetworkFileAbstraction.cs index 71ee70ba..136236ca 100644 --- a/FileLiberator/AaxcDownloadDecrypt/NetworkFileAbstraction.cs +++ b/AaxDecrypter/NetworkFileAbstraction.cs @@ -3,7 +3,7 @@ using System.IO; using System.Net.Http; using System.Threading.Tasks; -namespace FileLiberator.AaxcDownloadDecrypt +namespace AaxDecrypter { /// /// Provides a for a file over Http. 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/Cue.cs b/FileLiberator/AaxcDownloadDecrypt/Cue.cs deleted file mode 100644 index bf3de65e..00000000 --- a/FileLiberator/AaxcDownloadDecrypt/Cue.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.IO; -using System.Text; -using AudibleApiDTOs; -using Dinah.Core; - -namespace FileLiberator.AaxcDownloadDecrypt -{ - public static class Cue - { - public static string CreateContents(string filePath, ChapterInfo chapters) - { - var stringBuilder = new StringBuilder(); - - stringBuilder.AppendLine(GetFileLine(filePath, "MP3")); - - var trackCount = 0; - foreach (var c in chapters.Chapters) - { - trackCount++; - var startTime = TimeSpan.FromMilliseconds(c.StartOffsetMs); - - stringBuilder.AppendLine($"TRACK {trackCount} AUDIO"); - stringBuilder.AppendLine($" TITLE \"{c.Title}\""); - stringBuilder.AppendLine($" INDEX 01 {(int)startTime.TotalMinutes}:{startTime:ss\\:ff}"); - } - - return stringBuilder.ToString(); - } - - public static void UpdateFileName(FileInfo cueFileInfo, string audioFilePath) - => UpdateFileName(cueFileInfo.FullName, audioFilePath); - - public static void UpdateFileName(string cueFilePath, FileInfo audioFileInfo) - => UpdateFileName(cueFilePath, audioFileInfo.FullName); - - public static void UpdateFileName(FileInfo cueFileInfo, FileInfo audioFileInfo) - => UpdateFileName(cueFileInfo.FullName, audioFileInfo.FullName); - - public static void UpdateFileName(string cueFilePath, string audioFilePath) - { - var cueContents = File.ReadAllLines(cueFilePath); - - for (var i = 0; i < cueContents.Length; i++) - { - var line = cueContents[i]; - if (!line.Trim().StartsWith("FILE") || !line.Contains(" ")) - continue; - - var fileTypeBegins = line.LastIndexOf(" ") + 1; - cueContents[i] = GetFileLine(audioFilePath, line[fileTypeBegins..]); - break; - } - - File.WriteAllLines(cueFilePath, cueContents); - } - - private static string GetFileLine(string filePath, string audioType) => $"FILE {Path.GetFileName(filePath).SurroundWithQuotes()} {audioType}"; - } -} diff --git a/FileLiberator/AaxcDownloadDecrypt/DownloadDecryptBook.cs b/FileLiberator/AaxcDownloadDecrypt/DownloadDecryptBook.cs index 896663b9..926a6e53 100644 --- a/FileLiberator/AaxcDownloadDecrypt/DownloadDecryptBook.cs +++ b/FileLiberator/AaxcDownloadDecrypt/DownloadDecryptBook.cs @@ -8,6 +8,8 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using AaxDecrypter; +using AudibleApi; namespace FileLiberator.AaxcDownloadDecrypt { @@ -70,15 +72,21 @@ namespace FileLiberator.AaxcDownloadDecrypt var contentMetadata = await api.GetLibraryBookMetadataAsync(libraryBook.Book.AudibleProductId); + var aaxcDecryptDlLic = new AaxDecrypter.DownloadLicense(dlLic.DownloadUrl, dlLic.AudibleKey, dlLic.AudibleIV, Resources.UserAgent); - var newDownloader = await AaxcDownloadConverter.CreateAsync(Path.GetDirectoryName(destinationDir), dlLic, contentMetadata?.ChapterInfo); + var aaxcDecryptChapters = new AaxDecrypter.ChapterInfo(); + + foreach (var chap in contentMetadata?.ChapterInfo?.Chapters) + aaxcDecryptChapters.AddChapter(new Chapter(chap.Title, chap.StartOffsetMs, chap.LengthMs)); + + var newDownloader = await AaxcDownloadConverter.CreateAsync(Path.GetDirectoryName(destinationDir), aaxcDecryptDlLic, aaxcDecryptChapters); newDownloader.AppName = "Libation"; - TitleDiscovered?.Invoke(this, newDownloader.tags.title); - AuthorsDiscovered?.Invoke(this, newDownloader.tags.author); - NarratorsDiscovered?.Invoke(this, newDownloader.tags.narrator); - CoverImageFilepathDiscovered?.Invoke(this, newDownloader.tags.coverArt); + TitleDiscovered?.Invoke(this, newDownloader.Title); + AuthorsDiscovered?.Invoke(this, newDownloader.Author); + NarratorsDiscovered?.Invoke(this, newDownloader.Narrator); + CoverImageFilepathDiscovered?.Invoke(this, newDownloader.CoverArt); // override default which was set in CreateAsync var proposedOutputFile = Path.Combine(destinationDir, $"{libraryBook.Book.Title} [{libraryBook.Book.AudibleProductId}].m4b"); diff --git a/FileLiberator/AaxcDownloadDecrypt/FFMpegAaxcProcesser.cs b/FileLiberator/AaxcDownloadDecrypt/FFMpegAaxcProcesser.cs deleted file mode 100644 index 309dee35..00000000 --- a/FileLiberator/AaxcDownloadDecrypt/FFMpegAaxcProcesser.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - -namespace FileLiberator.AaxcDownloadDecrypt -{ - - /// - /// Download audible aaxc, decrypt, remux,and add metadata. - /// - class FFMpegAaxcProcesser - { - public event EventHandler ProgressUpdate; - public string FFMpegPath { get; } - public bool IsRunning { get; private set; } - public bool Succeeded { get; private set; } - - - private static Regex processedTimeRegex = new Regex("time=(\\d{2}):(\\d{2}):(\\d{2}).\\d{2}", RegexOptions.IgnoreCase | RegexOptions.Compiled); - public FFMpegAaxcProcesser(string ffmpegPath) - { - FFMpegPath = ffmpegPath; - } - - public async Task ProcessBook(string aaxcUrl, string userAgent, string audibleKey, string audibleIV, string metadataPath, string outputFile) - { - //This process gets the aaxc from the url and streams the decrypted - //aac stream to standard output - var downloader = new Process - { - StartInfo = getDownloaderStartInfo(aaxcUrl, userAgent, audibleKey, audibleIV) - }; - - //This process retreves an aac stream from standard input and muxes - // it into an m4b along with the cover art and metadata. - var remuxer = new Process - { - StartInfo = getRemuxerStartInfo(metadataPath, outputFile) - }; - - IsRunning = true; - - remuxer.ErrorDataReceived += Remuxer_ErrorDataReceived; - - downloader.Start(); - - var pipedOutput = downloader.StandardOutput.BaseStream; - - remuxer.Start(); - remuxer.BeginErrorReadLine(); - - var pipedInput = remuxer.StandardInput.BaseStream; - - int lastRead = 0; - - byte[] buffer = new byte[16 * 1024]; - - - //All the work done here. Copy download standard output into - //remuxer standard input - do - { - lastRead = await pipedOutput.ReadAsync(buffer, 0, buffer.Length); - await pipedInput.WriteAsync(buffer, 0, lastRead); - } while (lastRead > 0 && !remuxer.HasExited); - - 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; - } - private void Remuxer_ErrorDataReceived(object sender, DataReceivedEventArgs e) - { - if (!string.IsNullOrEmpty(e.Data) && 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); - } - } - - private ProcessStartInfo getDownloaderStartInfo(string aaxcUrl, string userAgent, string audibleKey, string audibleIV) => - new ProcessStartInfo - { - FileName = FFMpegPath, - RedirectStandardOutput = true, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - UseShellExecute = false, - WorkingDirectory = Path.GetDirectoryName(FFMpegPath), - ArgumentList ={ - "-nostdin", - "-audible_key", - audibleKey, - "-audible_iv", - audibleIV, - "-i", - aaxcUrl, - "-user_agent", - userAgent, //user-agent is requied for CDN to serve the file - "-c:a", //audio codec - "copy", //copy stream - "-f", //force output format: adts - "adts", - "pipe:" //pipe output to standard output - } - }; - - private ProcessStartInfo getRemuxerStartInfo(string metadataPath, string outputFile) => - new ProcessStartInfo - { - FileName = FFMpegPath, - RedirectStandardError = true, - RedirectStandardInput = true, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - UseShellExecute = false, - WorkingDirectory = Path.GetDirectoryName(FFMpegPath), - - ArgumentList = - { - "-thread_queue_size", - "1024", - "-f", //force input format: aac - "aac", - "-i", - "pipe:", //input from standard input - "-i", - metadataPath, - "-map", - "0", - "-map_metadata", - "1", - "-c", //codec copy - "copy", - "-f", //force output format: mp4 - "mp4", - outputFile, - "-y" //overwritte existing - } - }; - } -} diff --git a/FileLiberator/AaxcDownloadDecrypt/NFO.cs b/FileLiberator/AaxcDownloadDecrypt/NFO.cs deleted file mode 100644 index 409ec1e8..00000000 --- a/FileLiberator/AaxcDownloadDecrypt/NFO.cs +++ /dev/null @@ -1,56 +0,0 @@ -using AudibleApiDTOs; - -namespace FileLiberator.AaxcDownloadDecrypt -{ - public static class NFO - { - public static string CreateContents(string ripper, Tags tags, ChapterInfo chapters) - { - var _hours = (int)tags.duration.TotalHours; - var myDuration - = (_hours > 0 ? _hours + " hours, " : "") - + tags.duration.Minutes + " minutes, " - + tags.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"; - - var s - = header - + $" Publisher: {tags.publisher}\r\n" - + $" Duration: {myDuration}\r\n" - + $" Chapters: {chapters.Chapters.Length}\r\n" - + "\r\n" - + "\r\n" - + "Media Information\r\n" - + "=================\r\n" - + " Source Format: Audible AAX\r\n" - + $" Source Sample Rate: {tags.sampleRate} Hz\r\n" - + $" Source Channels: {tags.channels}\r\n" - + $" Source Bitrate: {tags.bitrate} kbits\r\n" - + "\r\n" - + " Lossless Encode: Yes\r\n" - + " Encoded Codec: AAC / M4B\r\n" - + $" Encoded Sample Rate: {tags.sampleRate} Hz\r\n" - + $" Encoded Channels: {tags.channels}\r\n" - + $" Encoded Bitrate: {tags.bitrate} kbits\r\n" - + "\r\n" - + $" Ripper: {ripper}\r\n" - + "\r\n" - + "\r\n" - + "Book Description\r\n" - + "================\r\n" - + tags.comments; - - return s; - } - } -} diff --git a/FileLiberator/AaxcDownloadDecrypt/Tags.cs b/FileLiberator/AaxcDownloadDecrypt/Tags.cs deleted file mode 100644 index 4c884a0b..00000000 --- a/FileLiberator/AaxcDownloadDecrypt/Tags.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using TagLib; -using TagLib.Mpeg4; -using Dinah.Core; - -namespace FileLiberator.AaxcDownloadDecrypt -{ - 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; } - public int channels { get; } - public int bitrate { get; } - public int sampleRate { get; } - - public bool hasCoverArt { get; } - public byte[] coverArt { get; } - - // input file - public Tags(TagLib.File tagLibFile) - { - 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 ?? ""; - 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; - - hasCoverArt = tagLibFile.Tag.Pictures.Length > 0; - if (hasCoverArt) - coverArt = tagLibFile.Tag.Pictures[0].Data.Data; - - duration = tagLibFile.Properties.Duration; - - bitrate = tagLibFile.Properties.AudioBitrate; - channels = tagLibFile.Properties.AudioChannels; - sampleRate = tagLibFile.Properties.AudioSampleRate; - } - - // 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/DecryptLib/AtomicParsley.exe b/FileLiberator/DecryptLib/AtomicParsley.exe deleted file mode 100644 index 47040f64..00000000 Binary files a/FileLiberator/DecryptLib/AtomicParsley.exe and /dev/null differ diff --git a/FileLiberator/DecryptLib/avcodec-58.dll b/FileLiberator/DecryptLib/avcodec-58.dll deleted file mode 100644 index 596f682a..00000000 Binary files a/FileLiberator/DecryptLib/avcodec-58.dll and /dev/null differ diff --git a/FileLiberator/DecryptLib/avdevice-58.dll b/FileLiberator/DecryptLib/avdevice-58.dll deleted file mode 100644 index f88b6c5c..00000000 Binary files a/FileLiberator/DecryptLib/avdevice-58.dll and /dev/null differ diff --git a/FileLiberator/DecryptLib/avfilter-7.dll b/FileLiberator/DecryptLib/avfilter-7.dll deleted file mode 100644 index 6cbedea3..00000000 Binary files a/FileLiberator/DecryptLib/avfilter-7.dll and /dev/null differ diff --git a/FileLiberator/DecryptLib/avformat-58.dll b/FileLiberator/DecryptLib/avformat-58.dll deleted file mode 100644 index 37161814..00000000 Binary files a/FileLiberator/DecryptLib/avformat-58.dll and /dev/null differ diff --git a/FileLiberator/DecryptLib/avutil-56.dll b/FileLiberator/DecryptLib/avutil-56.dll deleted file mode 100644 index d1565ae4..00000000 Binary files a/FileLiberator/DecryptLib/avutil-56.dll and /dev/null differ diff --git a/FileLiberator/DecryptLib/ffmpeg.exe b/FileLiberator/DecryptLib/ffmpeg.exe deleted file mode 100644 index f2840373..00000000 Binary files a/FileLiberator/DecryptLib/ffmpeg.exe and /dev/null differ diff --git a/FileLiberator/DecryptLib/ffprobe.exe b/FileLiberator/DecryptLib/ffprobe.exe deleted file mode 100644 index 6028bea1..00000000 Binary files a/FileLiberator/DecryptLib/ffprobe.exe and /dev/null differ diff --git a/FileLiberator/DecryptLib/swresample-3.dll b/FileLiberator/DecryptLib/swresample-3.dll deleted file mode 100644 index d2f5ea87..00000000 Binary files a/FileLiberator/DecryptLib/swresample-3.dll and /dev/null differ diff --git a/FileLiberator/DecryptLib/swscale-5.dll b/FileLiberator/DecryptLib/swscale-5.dll deleted file mode 100644 index a30c307e..00000000 Binary files a/FileLiberator/DecryptLib/swscale-5.dll and /dev/null differ diff --git a/FileLiberator/DecryptLib/taglib-sharp.dll b/FileLiberator/DecryptLib/taglib-sharp.dll deleted file mode 100644 index 87ec4bf1..00000000 Binary files a/FileLiberator/DecryptLib/taglib-sharp.dll and /dev/null differ diff --git a/FileLiberator/FileLiberator.csproj b/FileLiberator/FileLiberator.csproj index 526123a8..7aa586a6 100644 --- a/FileLiberator/FileLiberator.csproj +++ b/FileLiberator/FileLiberator.csproj @@ -19,40 +19,4 @@ - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - diff --git a/LibationLauncher/LibationLauncher.csproj b/LibationLauncher/LibationLauncher.csproj index 3b7129f2..3a2b4edd 100644 --- a/LibationLauncher/LibationLauncher.csproj +++ b/LibationLauncher/LibationLauncher.csproj @@ -13,7 +13,7 @@ win-x64 - 4.4.0.64 + 4.4.0.120