Made changes discussed in pull request.
This commit is contained in:
parent
0475bd48b1
commit
9930daa914
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}\"");
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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");
|
||||
}
|
||||
}
|
||||
24
AaxDecrypter/DownloadLicense.cs
Normal file
24
AaxDecrypter/DownloadLicense.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
AaxDecrypter/FFMpegAaxcProcesser.cs
Normal file
109
AaxDecrypter/FFMpegAaxcProcesser.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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.
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
@ -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}";
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
|
||||
@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user