Made changes discussed in pull request.

This commit is contained in:
Michael Bucari-Tovo 2021-06-26 01:39:18 -06:00
parent 0475bd48b1
commit 9930daa914
34 changed files with 275 additions and 1105 deletions

View File

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

View File

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

View File

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

View File

@ -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<int> 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<AaxcDownloadConverter> 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);
speedSamples.Enqueue(new DataRate
{
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;
}
private static string GenerateFfmpegChapters(ChapterInfo chapters)
private Queue<DataRate> speedSamples = new Queue<DataRate>(5);
private class DataRate
{
var stringBuilder = new System.Text.StringBuilder();
foreach (AudibleApiDTOs.Chapter c in chapters.Chapters)
{
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");
public DateTime SampleTime;
public TimeSpan ProcessPosition;
}
return stringBuilder.ToString();
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)
{
destTags.SetData(stag.BoxType, stag.Children.Cast<TagLib.Mpeg4.AppleDataBox>().ToArray());
}
public bool Step3_InsertCoverArt()
{
File.WriteAllBytes(coverArtPath, tags.coverArt);
var insertCoverArtInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = DecryptSupportLibraries.atomicParsleyPath,
Arguments = "\"" + outputFileName + "\" --encodingTool \"" + AppName + "\" --artwork \"" + coverArtPath + "\" --overWrite"
};
insertCoverArtInfo.RunHidden();
// delete temp file
FileExt.SafeDelete(coverArtPath);
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;
}
}
}

View File

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

View File

@ -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<Chapter> _chapterList = new();
private List<Chapter> _chapterList = new List<Chapter>();
public IEnumerable<Chapter> Chapters => _chapterList.AsEnumerable();
public int Count => _chapterList.Count;
public Chapter FirstChapter => _chapterList[0];
public Chapter LastChapter => _chapterList[Count - 1];
public IEnumerable<Chapter> ChapterList => _chapterList.AsEnumerable();
public IEnumerable<TimeSpan> 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<Chapter> chapters)
public string ToFFMeta()
{
_chapterList.AddRange(chapters);
var ffmetaChapters = new StringBuilder();
foreach (var c in Chapters)
{
ffmetaChapters.AppendLine(c.ToFFMeta());
}
public string GenerateFfmpegChapters()
return ffmetaChapters.ToString();
}
}
public class Chapter
{
var stringBuilder = new StringBuilder();
foreach (Chapter c in ChapterList)
public string Title { get; }
public long StartOffsetMs { get; }
public long EndOffsetMs { get; }
public Chapter(string title, long startOffsetMs, long lengthMs)
{
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");
Title = title;
StartOffsetMs = startOffsetMs;
EndOffsetMs = StartOffsetMs + lengthMs;
}
return stringBuilder.ToString();
public string ToFFMeta()
{
return "[CHAPTER]\n" +
"TIMEBASE=1/1000\n" +
"START=" + StartOffsetMs + "\n" +
"END=" + EndOffsetMs + "\n" +
"title=" + Title;
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.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}\"");

View File

@ -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");
}
}

View File

@ -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;
}
}
}

View File

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

View File

@ -0,0 +1,109 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace AaxDecrypter
{
/// <summary>
/// Download audible aaxc, decrypt, remux,and add metadata.
/// </summary>
class FFMpegAaxcProcesser
{
public event EventHandler<TimeSpan> 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();
}
}
}
}

View File

@ -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;
}

View File

@ -3,7 +3,7 @@ using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace FileLiberator.AaxcDownloadDecrypt
namespace AaxDecrypter
{
/// <summary>
/// Provides a <see cref="TagLib.File.IFileAbstraction"/> for a file over Http.

View File

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

View File

@ -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}";
}
}

View File

@ -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");

View File

@ -1,160 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace FileLiberator.AaxcDownloadDecrypt
{
/// <summary>
/// Download audible aaxc, decrypt, remux,and add metadata.
/// </summary>
class FFMpegAaxcProcesser
{
public event EventHandler<TimeSpan> 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
}
};
}
}

View File

@ -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;
}
}
}

View File

@ -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";
}
}

Binary file not shown.

Binary file not shown.

View File

@ -19,40 +19,4 @@
</Reference>
</ItemGroup>
<ItemGroup>
<None Update="DecryptLib\AtomicParsley.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\avcodec-58.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\avdevice-58.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\avfilter-7.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\avformat-58.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\avutil-56.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\ffmpeg.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\ffprobe.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\swresample-3.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\swscale-5.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DecryptLib\taglib-sharp.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

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