Merge pull request #51 from Mbucari/master

Replaced FFMpeg decryptor and taglib with AAXClean
This commit is contained in:
rmcrackan 2021-07-11 12:14:15 -04:00 committed by GitHub
commit 8240a97f6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 68 additions and 534 deletions

View File

@ -9,6 +9,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\AAXClean\AAXClean.csproj" />
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
</ItemGroup>

View File

@ -1,4 +1,5 @@
using Dinah.Core;
using AAXClean;
using Dinah.Core;
using Dinah.Core.Diagnostics;
using Dinah.Core.IO;
using Dinah.Core.StepRunner;
@ -9,7 +10,7 @@ namespace AaxDecrypter
{
public interface ISimpleAaxcToM4bConverter
{
event EventHandler<AaxcTagLibFile> RetrievedTags;
event EventHandler<AppleTags> RetrievedTags;
event EventHandler<byte[]> RetrievedCoverArt;
event EventHandler<TimeSpan> DecryptTimeRemaining;
event EventHandler<int> DecryptProgressUpdate;
@ -18,7 +19,7 @@ namespace AaxDecrypter
string outDir { get; }
string outputFileName { get; }
DownloadLicense downloadLicense { get; }
AaxcTagLibFile aaxcTagLib { get; }
Mp4File aaxFile { get; }
byte[] coverArt { get; }
void SetCoverArt(byte[] coverArt);
void SetOutputFilename(string outFileName);
@ -29,14 +30,13 @@ namespace AaxDecrypter
bool Step1_CreateDir();
bool Step2_GetMetadata();
bool Step3_DownloadAndCombine();
bool Step4_RestoreMetadata();
bool Step5_CreateCue();
bool Step6_CreateNfo();
bool Step7_Cleanup();
}
public class AaxcDownloadConverter : IAdvancedAaxcToM4bConverter
{
public event EventHandler<AaxcTagLibFile> RetrievedTags;
public event EventHandler<AppleTags> RetrievedTags;
public event EventHandler<byte[]> RetrievedCoverArt;
public event EventHandler<int> DecryptProgressUpdate;
public event EventHandler<TimeSpan> DecryptTimeRemaining;
@ -45,11 +45,11 @@ namespace AaxDecrypter
public string cacheDir { get; private set; }
public string outputFileName { get; private set; }
public DownloadLicense downloadLicense { get; private set; }
public AaxcTagLibFile aaxcTagLib { get; private set; }
public Mp4File aaxFile { get; private set; }
public byte[] coverArt { get; private set; }
private StepSequence steps { get; }
private FFMpegAaxcProcesser aaxcProcesser;
private NetworkFileStreamPersister nfsPersister;
private bool isCanceled { get; set; }
private string jsonDownloadState => Path.Combine(cacheDir, Path.GetFileNameWithoutExtension(outputFileName) + ".json");
private string tempFile => PathLib.ReplaceExtension(jsonDownloadState, ".aaxc");
@ -81,15 +81,11 @@ namespace AaxDecrypter
["Step 1: Create Dir"] = Step1_CreateDir,
["Step 2: Get Aaxc Metadata"] = Step2_GetMetadata,
["Step 3: Download Decrypted Audiobook"] = Step3_DownloadAndCombine,
["Step 4: Restore Aaxc Metadata"] = Step4_RestoreMetadata,
["Step 5: Create Cue"] = Step5_CreateCue,
["Step 6: Create Nfo"] = Step6_CreateNfo,
["Step 7: Cleanup"] = Step7_Cleanup,
};
aaxcProcesser = new FFMpegAaxcProcesser(dlLic);
aaxcProcesser.ProgressUpdate += AaxcProcesser_ProgressUpdate;
downloadLicense = dlLic;
}
@ -120,7 +116,7 @@ namespace AaxDecrypter
return false;
}
var speedup = (int)(aaxcTagLib.Properties.Duration.TotalSeconds / (long)Elapsed.TotalSeconds);
var speedup = (int)(aaxFile.Duration.TotalSeconds / (long)Elapsed.TotalSeconds);
Console.WriteLine("Speedup is " + speedup + "x realtime.");
Console.WriteLine("Done");
return true;
@ -137,8 +133,7 @@ namespace AaxDecrypter
public bool Step2_GetMetadata()
{
//Get metadata from the file over http
NetworkFileStreamPersister nfsPersister;
if (File.Exists(jsonDownloadState))
{
nfsPersister = new NetworkFileStreamPersister(jsonDownloadState);
@ -154,18 +149,12 @@ namespace AaxDecrypter
NetworkFileStream networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
nfsPersister = new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
}
nfsPersister.NetworkFileStream.BeginDownloading();
var networkFile = new NetworkFileAbstraction(nfsPersister.NetworkFileStream);
aaxcTagLib = new AaxcTagLibFile(networkFile);
nfsPersister.Dispose();
aaxFile = new Mp4File(nfsPersister.NetworkFileStream);
coverArt = aaxFile.AppleTags.Cover;
if (coverArt is null && aaxcTagLib.AppleTags.Pictures.Length > 0)
{
coverArt = aaxcTagLib.AppleTags.Pictures[0].Data.Data;
}
RetrievedTags?.Invoke(this, aaxcTagLib);
RetrievedTags?.Invoke(this, aaxFile.AppleTags);
RetrievedCoverArt?.Invoke(this, coverArt);
return !isCanceled;
@ -175,87 +164,40 @@ namespace AaxDecrypter
{
DecryptProgressUpdate?.Invoke(this, int.MaxValue);
NetworkFileStreamPersister nfsPersister;
if (File.Exists(jsonDownloadState))
{
nfsPersister = new NetworkFileStreamPersister(jsonDownloadState);
//If More thaan ~1 hour has elapsed since getting the download url, it will expire.
//The new url will be to the same file.
nfsPersister.NetworkFileStream.SetUriForSameFile(new Uri(downloadLicense.DownloadUrl));
}
else
{
var headers = new System.Net.WebHeaderCollection();
headers.Add("User-Agent", downloadLicense.UserAgent);
if (File.Exists(outputFileName))
FileExt.SafeDelete(outputFileName);
NetworkFileStream networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
nfsPersister = new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
}
FileStream outFile = File.OpenWrite(outputFileName);
string metadataPath = Path.Combine(outDir, Path.GetFileName(outputFileName) + ".ffmeta");
aaxFile.DecryptionProgressUpdate += AaxFile_DecryptionProgressUpdate;
var decryptedBook = aaxFile.DecryptAaxc(outFile, downloadLicense.AudibleKey, downloadLicense.AudibleIV, downloadLicense.ChapterInfo);
aaxFile.DecryptionProgressUpdate -= AaxFile_DecryptionProgressUpdate;
if (downloadLicense.ChapterInfo is null)
{
//If we want to keep the original chapters, we need to get them from the url.
//Ffprobe needs to seek to find metadata and it can't seek a pipe. Also, there's
//no guarantee that enough of the file will have been downloaded at this point
//to be able to use the cache file.
downloadLicense.ChapterInfo = new ChapterInfo(downloadLicense.DownloadUrl);
}
decryptedBook?.AppleTags?.SetCoverArt(coverArt);
decryptedBook?.Save();
decryptedBook?.Close();
//Only write chapters to the metadata file. All other aaxc metadata will be
//wiped out but is restored in Step 3.
File.WriteAllText(metadataPath, downloadLicense.ChapterInfo.ToFFMeta(true));
aaxcProcesser.ProcessBook(
nfsPersister.NetworkFileStream,
outputFileName,
metadataPath)
.GetAwaiter()
.GetResult();
nfsPersister.NetworkFileStream.Close();
nfsPersister.Dispose();
FileExt.SafeDelete(metadataPath);
DecryptProgressUpdate?.Invoke(this, 0);
return aaxcProcesser.Succeeded && !isCanceled;
return aaxFile is not null && !isCanceled;
}
private void AaxcProcesser_ProgressUpdate(object sender, AaxcProcessUpdate e)
private void AaxFile_DecryptionProgressUpdate(object sender, DecryptionProgressEventArgs e)
{
double remainingSecsToProcess = (aaxcTagLib.Properties.Duration - e.ProcessPosition).TotalSeconds;
var duration = aaxFile.Duration;
double remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
if (double.IsNormal(estTimeRemaining))
DecryptTimeRemaining?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining));
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / aaxcTagLib.Properties.Duration.TotalSeconds;
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
DecryptProgressUpdate?.Invoke(this, (int)progressPercent);
}
/// <summary>
/// Copy all aacx metadata to m4b file, including cover art.
/// </summary>
public bool Step4_RestoreMetadata()
{
var outFile = new AaxcTagLibFile(outputFileName);
outFile.CopyTagsFrom(aaxcTagLib);
if (outFile.AppleTags.Pictures.Length == 0 && coverArt is not null)
{
outFile.AddPicture(coverArt);
}
outFile.Save();
return !isCanceled;
}
public bool Step5_CreateCue()
{
try
@ -273,7 +215,7 @@ namespace AaxDecrypter
{
try
{
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxcTagLib, downloadLicense.ChapterInfo));
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxFile, downloadLicense.ChapterInfo));
}
catch (Exception ex)
{
@ -292,7 +234,7 @@ namespace AaxDecrypter
public void Cancel()
{
isCanceled = true;
aaxcProcesser.Cancel();
aaxFile?.Cancel();
}
}
}

View File

@ -1,75 +0,0 @@
using System;
using System.Linq;
using Dinah.Core;
using TagLib;
using TagLib.Mpeg4;
namespace AaxDecrypter
{
public class AaxcTagLibFile : TagLib.Mpeg4.File
{
// ©
private const byte COPYRIGHT = 0xa9;
private static ReadOnlyByteVector narratorType { get; } = new ReadOnlyByteVector(COPYRIGHT, (byte)'n', (byte)'r', (byte)'t');
private static ReadOnlyByteVector descriptionType { get; } = new ReadOnlyByteVector(COPYRIGHT, (byte)'d', (byte)'e', (byte)'s');
private static ReadOnlyByteVector publisherType { get; } = new ReadOnlyByteVector(COPYRIGHT, (byte)'p', (byte)'u', (byte)'b');
public string AsciiTitleSansUnabridged => TitleSansUnabridged?.UnicodeToAscii();
public string AsciiFirstAuthor => FirstAuthor?.UnicodeToAscii();
public string AsciiNarrator => Narrator?.UnicodeToAscii();
public string AsciiComment => Comment?.UnicodeToAscii();
public string AsciiLongDescription => LongDescription?.UnicodeToAscii();
public AppleTag AppleTags => GetTag(TagTypes.Apple) as AppleTag;
public string Comment => AppleTags.Comment;
public string[] Authors => AppleTags.Performers;
public string FirstAuthor => Authors?.Length > 0 ? Authors[0] : default;
public string TitleSansUnabridged => AppleTags.Title?.Replace(" (Unabridged)", "");
public string BookCopyright => _copyright is not null && _copyright.Length > 0 ? _copyright[0] : default;
public string RecordingCopyright => _copyright is not null && _copyright.Length > 1 ? _copyright[1] : default;
private string[] _copyright => AppleTags.Copyright?.Replace("&#169;", string.Empty)?.Replace("(P)", string.Empty)?.Split(';');
public string Narrator => getAppleTagsText(narratorType);
public string LongDescription => getAppleTagsText(descriptionType);
public string ReleaseDate => getAppleTagsText("rldt");
public string Publisher => getAppleTagsText(publisherType);
private string getAppleTagsText(ByteVector byteVector)
{
string[] text = AppleTags.GetText(byteVector);
return text.Length == 0 ? default : text[0];
}
public AaxcTagLibFile(IFileAbstraction abstraction)
: base(abstraction, ReadStyle.Average)
{
}
public AaxcTagLibFile(string path)
: this(new LocalFileAbstraction(path))
{
}
/// <summary>
/// Copy all metadata fields in the source file, even those that TagLib doesn't
/// recognize, to the output file.
/// NOTE: Chapters aren't stored in MPEG-4 metadata. They are encoded as a Timed
/// Text Stream (MPEG-4 Part 17), so taglib doesn't read or write them.
/// </summary>
/// <param name="sourceFile">File from which tags will be coppied.</param>
public void CopyTagsFrom(AaxcTagLibFile sourceFile)
{
AppleTags.Clear();
foreach (var stag in sourceFile.AppleTags)
{
AppleTags.SetData(stag.BoxType, stag.Children.Cast<AppleDataBox>().ToArray());
}
}
public void AddPicture(byte[] coverArt)
{
AppleTags.SetData("covr", coverArt, 0);
}
}
}

View File

@ -1,86 +0,0 @@
using Dinah.Core;
using Dinah.Core.Diagnostics;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
namespace AaxDecrypter
{
public class ChapterInfo
{
private List<Chapter> _chapterList = new List<Chapter>();
public IEnumerable<Chapter> Chapters => _chapterList.AsEnumerable();
public int Count => _chapterList.Count;
public ChapterInfo() { }
public ChapterInfo(string audiobookFile)
{
var info = new ProcessStartInfo
{
FileName = DecryptSupportLibraries.ffprobePath,
Arguments = "-loglevel panic -show_chapters -print_format json \"" + audiobookFile + "\""
};
var jString = info.RunHidden().Output;
var chapterJObject = JObject.Parse(jString);
var chapters = chapterJObject["chapters"]
.Select(c => new Chapter(
c["tags"]?["title"]?.Value<string>(),
c["start_time"].Value<double>(),
c["end_time"].Value<double>()
));
_chapterList.AddRange(chapters);
}
public void AddChapter(Chapter chapter)
{
ArgumentValidator.EnsureNotNull(chapter, nameof(chapter));
_chapterList.Add(chapter);
}
public string ToFFMeta(bool includeFFMetaHeader)
{
var ffmetaChapters = new StringBuilder();
if (includeFFMetaHeader)
ffmetaChapters.AppendLine(";FFMETADATA1");
foreach (var c in Chapters)
{
ffmetaChapters.AppendLine(c.ToFFMeta());
}
return ffmetaChapters.ToString();
}
}
public class Chapter
{
public string Title { get; }
public TimeSpan StartOffset { get; }
public TimeSpan EndOffset { get; }
public Chapter(string title, long startOffsetMs, long lengthMs)
{
ArgumentValidator.EnsureNotNullOrEmpty(title, nameof(title));
ArgumentValidator.EnsureGreaterThan(startOffsetMs, nameof(startOffsetMs), -1);
// do not validate lengthMs for '> 0'. It is valid to set sections this way. eg: 11-22-63 [B005UR3VFO] by Stephen King
Title = title;
StartOffset = TimeSpan.FromMilliseconds(startOffsetMs);
EndOffset = StartOffset + TimeSpan.FromMilliseconds(lengthMs);
}
public Chapter(string title, double startTimeSec, double endTimeSec)
: this(title, (long)(startTimeSec * 1000), (long)((endTimeSec - startTimeSec) * 1000))
{
}
public string ToFFMeta()
{
return "[CHAPTER]\n" +
"TIMEBASE=1/1000\n" +
"START=" + StartOffset.TotalMilliseconds + "\n" +
"END=" + EndOffset.TotalMilliseconds + "\n" +
"title=" + Title;
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Text;
using AAXClean;
using Dinah.Core;
namespace AaxDecrypter

View File

@ -1,4 +1,5 @@
using Dinah.Core;
using AAXClean;
using Dinah.Core;
namespace AaxDecrypter
{

View File

@ -1,254 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace AaxDecrypter
{
class AaxcProcessUpdate
{
public AaxcProcessUpdate(TimeSpan position, double speed)
{
ProcessPosition = position;
ProcessSpeed = speed;
EventTime = DateTime.Now;
}
public TimeSpan ProcessPosition { get; }
public double ProcessSpeed { get; }
public DateTime EventTime { get; }
}
/// <summary>
/// Download audible aaxc, decrypt, and remux with chapters.
/// </summary>
class FFMpegAaxcProcesser
{
public event EventHandler<AaxcProcessUpdate> ProgressUpdate;
public string FFMpegPath { get; }
public DownloadLicense DownloadLicense { get; }
public bool IsRunning { get; private set; }
public bool Succeeded { get; private set; }
public string FFMpegRemuxerStandardError => remuxerError.ToString();
public string FFMpegDecrypterStandardError => decrypterError.ToString();
private StringBuilder remuxerError { get; } = new StringBuilder();
private StringBuilder decrypterError { get; } = new StringBuilder();
private static Regex processedTimeRegex { get; } = new Regex("time=(\\d{2}):(\\d{2}):(\\d{2}).\\d{2}.*speed=\\s{0,1}([0-9]*[.]?[0-9]+)(?:e\\+([0-9]+)){0,1}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private Process decrypter;
private Process remuxer;
private Stream inputFile;
private bool isCanceled = false;
public FFMpegAaxcProcesser(DownloadLicense downloadLicense)
{
FFMpegPath = DecryptSupportLibraries.ffmpegPath;
DownloadLicense = downloadLicense;
}
public async Task ProcessBook(Stream inputFile, string outputFile, string ffmetaChaptersPath)
{
this.inputFile = inputFile;
//This process gets the aaxc from the url and streams the decrypted
//aac stream to standard output
decrypter = new Process
{
StartInfo = getDownloaderStartInfo()
};
//This process retreves an aac stream from standard input and muxes
// it into an m4b along with the cover art and metadata.
remuxer = new Process
{
StartInfo = getRemuxerStartInfo(outputFile, ffmetaChaptersPath)
};
IsRunning = true;
decrypter.ErrorDataReceived += Downloader_ErrorDataReceived;
decrypter.Start();
decrypter.BeginErrorReadLine();
remuxer.ErrorDataReceived += Remuxer_ErrorDataReceived;
remuxer.Start();
remuxer.BeginErrorReadLine();
//Thic check needs to be placed after remuxer has started.
if (isCanceled) return;
var decrypterInput = decrypter.StandardInput.BaseStream;
var decrypterOutput = decrypter.StandardOutput.BaseStream;
var remuxerInput = remuxer.StandardInput.BaseStream;
//Read inputFile into decrypter stdin in the background
var t = new Thread(() => CopyStream(inputFile, decrypterInput, decrypter));
t.Start();
//All the work done here. Copy download standard output into
//remuxer standard input
await Task.Run(() => CopyStream(decrypterOutput, remuxerInput, remuxer));
//If the remuxer exited due to failure, downloader will still have
//data in the pipe. Force kill downloader to continue.
if (remuxer.HasExited && !decrypter.HasExited)
decrypter.Kill();
remuxer.WaitForExit();
decrypter.WaitForExit();
IsRunning = false;
Succeeded = decrypter.ExitCode == 0 && remuxer.ExitCode == 0;
}
private void CopyStream(Stream inputStream, Stream outputStream, Process returnOnProcExit)
{
try
{
byte[] buffer = new byte[32 * 1024];
int lastRead;
do
{
lastRead = inputStream.Read(buffer, 0, buffer.Length);
outputStream.Write(buffer, 0, lastRead);
} while (lastRead > 0 && !returnOnProcExit.HasExited);
}
catch (IOException ex)
{
//There is no way to tell if the process closed the input stream
//before trying to write to it. If it did close, throws IOException.
isCanceled = true;
}
finally
{
outputStream.Close();
}
}
public void Cancel()
{
isCanceled = true;
if (IsRunning && !remuxer.HasExited)
remuxer.Kill();
if (IsRunning && !decrypter.HasExited)
decrypter.Kill();
inputFile?.Close();
}
private void Downloader_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (string.IsNullOrEmpty(e.Data))
return;
decrypterError.AppendLine(e.Data);
}
private void Remuxer_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (string.IsNullOrEmpty(e.Data))
return;
remuxerError.AppendLine(e.Data);
if (processedTimeRegex.IsMatch(e.Data))
{
//get timestamp of of last processed audio stream position
//and processing speed
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);
double speed = double.Parse(match.Groups[4].Value);
int exp = match.Groups[5].Success ? int.Parse(match.Groups[5].Value) : 0;
speed *= Math.Pow(10, exp);
ProgressUpdate?.Invoke(this, new AaxcProcessUpdate(position, speed));
}
if (e.Data.Contains("aac bitstream error"))
{
//This happens if input is corrupt (should never happen) or if caller
//supplied wrong key/iv
var process = sender as Process;
process.Kill();
}
}
private ProcessStartInfo getDownloaderStartInfo() =>
new ProcessStartInfo
{
FileName = FFMpegPath,
RedirectStandardError = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
ArgumentList ={
"-audible_key",
DownloadLicense.AudibleKey,
"-audible_iv",
DownloadLicense.AudibleIV,
"-f",
"mp4",
"-i",
"pipe:",
"-c:a", //audio codec
"copy", //copy stream
"-f", //force output format: adts
"adts",
"pipe:" //pipe output to stdout
}
};
private ProcessStartInfo getRemuxerStartInfo(string outputFile, string ffmetaChaptersPath = null)
{
var startInfo = new ProcessStartInfo
{
FileName = FFMpegPath,
RedirectStandardError = true,
RedirectStandardInput = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
};
startInfo.ArgumentList.Add("-thread_queue_size");
startInfo.ArgumentList.Add("1024");
startInfo.ArgumentList.Add("-f"); //force input format: aac
startInfo.ArgumentList.Add("aac");
startInfo.ArgumentList.Add("-i"); //read input from stdin
startInfo.ArgumentList.Add("pipe:");
//copy metadata from supplied metadata file
startInfo.ArgumentList.Add("-f");
startInfo.ArgumentList.Add("ffmetadata");
startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(ffmetaChaptersPath);
startInfo.ArgumentList.Add("-map"); //map file 0 (aac audio stream)
startInfo.ArgumentList.Add("0");
startInfo.ArgumentList.Add("-map_chapters"); //copy chapter data from file metadata file
startInfo.ArgumentList.Add("1");
startInfo.ArgumentList.Add("-c"); //copy all mapped streams
startInfo.ArgumentList.Add("copy");
startInfo.ArgumentList.Add("-f"); //force output format: mp4
startInfo.ArgumentList.Add("mp4");
startInfo.ArgumentList.Add("-movflags");
startInfo.ArgumentList.Add("disable_chpl"); //Disable Nero chapters format
startInfo.ArgumentList.Add(outputFile);
startInfo.ArgumentList.Add("-y"); //overwrite existing
return startInfo;
}
}
}

View File

@ -1,27 +1,29 @@

using AAXClean;
using Dinah.Core;
namespace AaxDecrypter
{
public static class NFO
{
public static string CreateContents(string ripper, AaxcTagLibFile aaxcTagLib, ChapterInfo chapters)
public static string CreateContents(string ripper, AAXClean.Mp4File aaxcTagLib, ChapterInfo chapters)
{
var _hours = (int)aaxcTagLib.Properties.Duration.TotalHours;
var _hours = (int)aaxcTagLib.Duration.TotalHours;
var myDuration
= (_hours > 0 ? _hours + " hours, " : string.Empty)
+ aaxcTagLib.Properties.Duration.Minutes + " minutes, "
+ aaxcTagLib.Properties.Duration.Seconds + " seconds";
+ aaxcTagLib.Duration.Minutes + " minutes, "
+ aaxcTagLib.Duration.Seconds + " seconds";
var nfoString
= "General Information\r\n"
+ "======================\r\n"
+ $" Title: {aaxcTagLib.AsciiTitleSansUnabridged ?? "[unknown]"}\r\n"
+ $" Author: {aaxcTagLib.AsciiFirstAuthor ?? "[unknown]"}\r\n"
+ $" Read By: {aaxcTagLib.AsciiNarrator ?? "[unknown]"}\r\n"
+ $" Release Date: {aaxcTagLib.ReleaseDate ?? "[unknown]"}\r\n"
+ $" Book Copyright: {aaxcTagLib.BookCopyright ?? "[unknown]"}\r\n"
+ $" Recording Copyright: {aaxcTagLib.RecordingCopyright ?? "[unknown]"}\r\n"
+ $" Genre: {aaxcTagLib.AppleTags.FirstGenre ?? "[unknown]"}\r\n"
+ $" Publisher: {aaxcTagLib.Publisher ?? "[unknown]"}\r\n"
+ $" Title: {aaxcTagLib.AppleTags.TitleSansUnabridged?.UnicodeToAscii() ?? "[unknown]"}\r\n"
+ $" Author: {aaxcTagLib.AppleTags.FirstAuthor?.UnicodeToAscii() ?? "[unknown]"}\r\n"
+ $" Read By: {aaxcTagLib.AppleTags.Narrator?.UnicodeToAscii() ?? "[unknown]"}\r\n"
+ $" Release Date: {aaxcTagLib.AppleTags.ReleaseDate ?? "[unknown]"}\r\n"
+ $" Book Copyright: {aaxcTagLib.AppleTags.BookCopyright ?? "[unknown]"}\r\n"
+ $" Recording Copyright: {aaxcTagLib.AppleTags.RecordingCopyright ?? "[unknown]"}\r\n"
+ $" Genre: {aaxcTagLib.AppleTags.Generes ?? "[unknown]"}\r\n"
+ $" Publisher: {aaxcTagLib.AppleTags.Publisher ?? "[unknown]"}\r\n"
+ $" Duration: {myDuration}\r\n"
+ $" Chapters: {chapters.Count}\r\n"
+ "\r\n"
@ -29,22 +31,22 @@ namespace AaxDecrypter
+ "Media Information\r\n"
+ "======================\r\n"
+ " Source Format: Audible AAX\r\n"
+ $" Source Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n"
+ $" Source Channels: {aaxcTagLib.Properties.AudioChannels}\r\n"
+ $" Source Bitrate: {aaxcTagLib.Properties.AudioBitrate} Kbps\r\n"
+ $" Source Sample Rate: {aaxcTagLib.TimeScale} Hz\r\n"
+ $" Source Channels: {aaxcTagLib.AudioChannels}\r\n"
+ $" Source Bitrate: {aaxcTagLib.AverageBitrate} Kbps\r\n"
+ "\r\n"
+ " Lossless Encode: Yes\r\n"
+ " Encoded Codec: AAC / M4B\r\n"
+ $" Encoded Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n"
+ $" Encoded Channels: {aaxcTagLib.Properties.AudioChannels}\r\n"
+ $" Encoded Bitrate: {aaxcTagLib.Properties.AudioBitrate} Kbps\r\n"
+ $" Encoded Sample Rate: {aaxcTagLib.TimeScale} Hz\r\n"
+ $" Encoded Channels: {aaxcTagLib.AudioChannels}\r\n"
+ $" Encoded Bitrate: {aaxcTagLib.AverageBitrate} Kbps\r\n"
+ "\r\n"
+ $" Ripper: {ripper}\r\n"
+ "\r\n"
+ "\r\n"
+ "Book Description\r\n"
+ "================\r\n"
+ (!string.IsNullOrWhiteSpace(aaxcTagLib.LongDescription) ? aaxcTagLib.AsciiLongDescription : aaxcTagLib.AsciiComment);
+ (!string.IsNullOrWhiteSpace(aaxcTagLib.AppleTags.LongDescription) ? aaxcTagLib.AppleTags.LongDescription.UnicodeToAscii() : aaxcTagLib.AppleTags.Comment?.UnicodeToAscii());
return nfoString;
}

View File

@ -176,7 +176,7 @@ namespace AaxDecrypter
/// <summary>
/// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread.
/// </summary>
private void BeginDownloading()
public void BeginDownloading()
{
if (ContentLength != 0 && WritePosition == ContentLength)
{
@ -393,7 +393,7 @@ namespace AaxDecrypter
//read operation will block until file contains enough data
//to fulfil the request, or until cancelled.
while (requiredPosition > WritePosition && !isCancelled)
Thread.Sleep(0);
Thread.Sleep(2);
return _readFile.Read(buffer, offset, count);
}

View File

@ -81,15 +81,10 @@ namespace FileLiberator
if (Configuration.Instance.AllowLibationFixup)
{
aaxcDecryptDlLic.ChapterInfo = new ChapterInfo();
aaxcDecryptDlLic.ChapterInfo = new AAXClean.ChapterInfo();
foreach (var chap in contentLic.ContentMetadata?.ChapterInfo?.Chapters)
aaxcDecryptDlLic.ChapterInfo.AddChapter(
new Chapter(
chap.Title,
chap.StartOffsetMs,
chap.LengthMs
));
aaxcDecryptDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs));
}
aaxcDownloader = AaxcDownloadConverter.Create(cacheDir, destinationDir, aaxcDecryptDlLic);
@ -132,7 +127,7 @@ namespace FileLiberator
}
}
private void aaxcDownloader_RetrievedTags(object sender, AaxcTagLibFile e)
private void aaxcDownloader_RetrievedTags(object sender, AAXClean.AppleTags e)
{
TitleDiscovered?.Invoke(this, e.TitleSansUnabridged);
AuthorsDiscovered?.Invoke(this, e.FirstAuthor ?? "[unknown]");

View File

@ -86,6 +86,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine.Tests"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore.Tests", "..\Dinah.Core\_Tests\Dinah.EntityFrameworkCore.Tests\Dinah.EntityFrameworkCore.Tests.csproj", "{6F5131A0-09AE-4707-B82B-5E53CB74688E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AAXClean", "..\AAXClean\AAXClean.csproj", "{94BEB7CC-511D-45AB-9F09-09BE858EE486}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -208,6 +210,10 @@ Global
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Release|Any CPU.Build.0 = Release|Any CPU
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Debug|Any CPU.Build.0 = Debug|Any CPU
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Release|Any CPU.ActiveCfg = Release|Any CPU
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -242,6 +248,7 @@ Global
{8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{6F5131A0-09AE-4707-B82B-5E53CB74688E} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{94BEB7CC-511D-45AB-9F09-09BE858EE486} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>