split AaxcDownload single and multi

This commit is contained in:
Robert McRackan 2021-10-18 14:41:57 -04:00
parent 0f1ff0aa10
commit 2767f04621
7 changed files with 293 additions and 260 deletions

View File

@ -0,0 +1,91 @@
using System;
using AAXClean;
using Dinah.Core.Net.Http;
namespace AaxDecrypter
{
public abstract class AaxcDownloadConvertBase : AudiobookDownloadBase
{
protected OutputFormat OutputFormat { get; }
protected AaxFile AaxFile;
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
: base(outFileName, cacheDirectory, dlLic)
{
OutputFormat = outputFormat;
}
/// <summary>Setting cover art by this method will insert the art into the audiobook metadata</summary>
public override void SetCoverArt(byte[] coverArt)
{
base.SetCoverArt(coverArt);
if (coverArt is not null)
AaxFile?.AppleTags.SetCoverArt(coverArt);
}
protected bool Step_GetMetadata()
{
AaxFile = new AaxFile(InputFileStream);
OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged);
OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor ?? "[unknown]");
OnRetrievedNarrators(AaxFile.AppleTags.Narrator ?? "[unknown]");
OnRetrievedCoverArt(AaxFile.AppleTags.Cover);
return !IsCanceled;
}
protected DownloadProgress Step_DownloadAudiobook_Start()
{
var zeroProgress = new DownloadProgress
{
BytesReceived = 0,
ProgressPercentage = 0,
TotalBytesToReceive = InputFileStream.Length
};
OnDecryptProgressUpdate(zeroProgress);
AaxFile.SetDecryptionKey(DownloadLicense.AudibleKey, DownloadLicense.AudibleIV);
return zeroProgress;
}
protected void Step_DownloadAudiobook_End(DownloadProgress zeroProgress)
{
AaxFile.Close();
CloseInputFileStream();
OnDecryptProgressUpdate(zeroProgress);
}
protected void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
{
var duration = AaxFile.Duration;
var remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
var estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
if (double.IsNormal(estTimeRemaining))
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
var progressPercent = e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
OnDecryptProgressUpdate(
new DownloadProgress
{
ProgressPercentage = 100 * progressPercent,
BytesReceived = (long)(InputFileStream.Length * progressPercent),
TotalBytesToReceive = InputFileStream.Length
});
}
public override void Cancel()
{
IsCanceled = true;
AaxFile?.Cancel();
AaxFile?.Dispose();
CloseInputFileStream();
}
}
}

View File

@ -1,238 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AAXClean;
using Dinah.Core.Net.Http;
using Dinah.Core.StepRunner;
using FileManager;
namespace AaxDecrypter
{
public class AaxcDownloadConverter : AudiobookDownloadBase
{
private static readonly TimeSpan minChapterLength = TimeSpan.FromSeconds(3);
protected override StepSequence Steps { get; }
private AaxFile aaxFile;
private OutputFormat OutputFormat { get; }
private List<string> multiPartFilePaths { get; } = new List<string>();
public AaxcDownloadConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat, bool splitFileByChapters)
: base(outFileName, cacheDirectory, dlLic)
{
OutputFormat = outputFormat;
Steps = new StepSequence
{
Name = "Download and Convert Aaxc To " + OutputFormat,
["Step 1: Get Aaxc Metadata"] = Step1_GetMetadata,
["Step 2: Download Decrypted Audiobook"] = splitFileByChapters
? Step2_DownloadAudiobookAsMultipleFilesPerChapter
: Step2_DownloadAudiobookAsSingleFile,
["Step 3: Create Cue"] = splitFileByChapters
? () => true
: Step3_CreateCue,
["Step 4: Cleanup"] = Step4_Cleanup,
};
}
/// <summary>
/// Setting cover art by this method will insert the art into the audiobook metadata
/// </summary>
public override void SetCoverArt(byte[] coverArt)
{
base.SetCoverArt(coverArt);
aaxFile?.AppleTags.SetCoverArt(coverArt);
}
protected override bool Step1_GetMetadata()
{
aaxFile = new AaxFile(InputFileStream);
OnRetrievedTitle(aaxFile.AppleTags.TitleSansUnabridged);
OnRetrievedAuthors(aaxFile.AppleTags.FirstAuthor ?? "[unknown]");
OnRetrievedNarrators(aaxFile.AppleTags.Narrator ?? "[unknown]");
OnRetrievedCoverArt(aaxFile.AppleTags.Cover);
return !IsCanceled;
}
private bool Step2_DownloadAudiobookAsSingleFile()
{
var zeroProgress = Step2_Start();
FileUtility.SaferDelete(OutputFileName);
var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
aaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
var decryptionResult = OutputFormat == OutputFormat.M4b ? aaxFile.ConvertToMp4a(outputFile, DownloadLicense.ChapterInfo) : aaxFile.ConvertToMp3(outputFile);
aaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
DownloadLicense.ChapterInfo = aaxFile.Chapters;
Step2_End(zeroProgress);
var success = decryptionResult == ConversionResult.NoErrorsDetected && !IsCanceled;
if (success)
base.OnFileCreated(OutputFileName);
return success;
}
/*
https://github.com/rmcrackan/Libation/pull/127#issuecomment-939088489
If the chapter truly is empty, that is, 0 audio frames in length, then yes it is ignored.
If the chapter is shorter than 3 seconds long but still has some audio frames, those frames are combined with the following chapter and not split into a new file.
I also implemented file naming by chapter title. When 2 or more consecutive chapters are combined, the first of the combined chapter's title is used in the file name. For example, given an audiobook with the following chapters:
00:00:00 - 00:00:02 | Part 1
00:00:02 - 00:35:00 | Chapter 1
00:35:02 - 01:02:00 | Chapter 2
01:02:00 - 01:02:02 | Part 2
01:02:02 - 01:41:00 | Chapter 3
01:41:00 - 02:05:00 | Chapter 4
The book will be split into the following files:
00:00:00 - 00:35:00 | Book - 01 - Part 1.m4b
00:35:00 - 01:02:00 | Book - 02 - Chapter 2.m4b
01:02:00 - 01:41:00 | Book - 03 - Part 2.m4b
01:41:00 - 02:05:00 | Book - 04 - Chapter 4.m4b
That naming may not be desirable for everyone, but it's an easy change to instead use the last of the combined chapter's title in the file name.
*/
private bool Step2_DownloadAudiobookAsMultipleFilesPerChapter()
{
var zeroProgress = Step2_Start();
var chapters = DownloadLicense.ChapterInfo.Chapters.ToList();
//Ensure split files are at least minChapterLength in duration.
var splitChapters = new ChapterInfo();
var runningTotal = TimeSpan.Zero;
string title = "";
for (int i = 0; i < chapters.Count; i++)
{
if (runningTotal == TimeSpan.Zero)
title = chapters[i].Title;
runningTotal += chapters[i].Duration;
if (runningTotal >= minChapterLength)
{
splitChapters.AddChapter(title, runningTotal);
runningTotal = TimeSpan.Zero;
}
}
// reset, just in case
multiPartFilePaths.Clear();
aaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
if (OutputFormat == OutputFormat.M4b)
ConvertToMultiMp4b(splitChapters);
else
ConvertToMultiMp3(splitChapters);
aaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
Step2_End(zeroProgress);
var success = !IsCanceled;
if (success)
foreach (var path in multiPartFilePaths)
OnFileCreated(path);
return success;
}
private DownloadProgress Step2_Start()
{
var zeroProgress = new DownloadProgress
{
BytesReceived = 0,
ProgressPercentage = 0,
TotalBytesToReceive = InputFileStream.Length
};
OnDecryptProgressUpdate(zeroProgress);
aaxFile.SetDecryptionKey(DownloadLicense.AudibleKey, DownloadLicense.AudibleIV);
return zeroProgress;
}
private void Step2_End(DownloadProgress zeroProgress)
{
aaxFile.Close();
CloseInputFileStream();
OnDecryptProgressUpdate(zeroProgress);
}
private void ConvertToMultiMp4b(ChapterInfo splitChapters)
{
var chapterCount = 0;
aaxFile.ConvertToMultiMp4a(splitChapters, newSplitCallback =>
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback)
);
}
private void ConvertToMultiMp3(ChapterInfo splitChapters)
{
var chapterCount = 0;
aaxFile.ConvertToMultiMp3(splitChapters, newSplitCallback =>
{
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback);
newSplitCallback.LameConfig.ID3.Track = chapterCount.ToString();
});
}
private void createOutputFileStream(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
{
var fileName = FileUtility.GetMultipartFileName(OutputFileName, currentChapter, splitChapters.Count, newSplitCallback.Chapter.Title);
multiPartFilePaths.Add(fileName);
FileUtility.SaferDelete(fileName);
newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate);
}
private void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
{
var duration = aaxFile.Duration;
double remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
if (double.IsNormal(estTimeRemaining))
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
double progressPercent = e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
OnDecryptProgressUpdate(
new DownloadProgress
{
ProgressPercentage = 100 * progressPercent,
BytesReceived = (long)(InputFileStream.Length * progressPercent),
TotalBytesToReceive = InputFileStream.Length
});
}
public override void Cancel()
{
IsCanceled = true;
aaxFile?.Cancel();
aaxFile?.Dispose();
CloseInputFileStream();
}
}
}

View File

@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AAXClean;
using Dinah.Core.StepRunner;
using FileManager;
namespace AaxDecrypter
{
public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase
{
protected override StepSequence Steps { get; }
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
private List<string> multiPartFilePaths { get; } = new List<string>();
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
: base(outFileName, cacheDirectory, dlLic, outputFormat)
{
Steps = new StepSequence
{
Name = "Download and Convert Aaxc To " + OutputFormat,
["Step 1: Get Aaxc Metadata"] = Step_GetMetadata,
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsMultipleFilesPerChapter,
["Step 3: Cleanup"] = Step_Cleanup,
};
}
/*
https://github.com/rmcrackan/Libation/pull/127#issuecomment-939088489
If the chapter truly is empty, that is, 0 audio frames in length, then yes it is ignored.
If the chapter is shorter than 3 seconds long but still has some audio frames, those frames are combined with the following chapter and not split into a new file.
I also implemented file naming by chapter title. When 2 or more consecutive chapters are combined, the first of the combined chapter's title is used in the file name. For example, given an audiobook with the following chapters:
00:00:00 - 00:00:02 | Part 1
00:00:02 - 00:35:00 | Chapter 1
00:35:02 - 01:02:00 | Chapter 2
01:02:00 - 01:02:02 | Part 2
01:02:02 - 01:41:00 | Chapter 3
01:41:00 - 02:05:00 | Chapter 4
The book will be split into the following files:
00:00:00 - 00:35:00 | Book - 01 - Part 1.m4b
00:35:00 - 01:02:00 | Book - 02 - Chapter 2.m4b
01:02:00 - 01:41:00 | Book - 03 - Part 2.m4b
01:41:00 - 02:05:00 | Book - 04 - Chapter 4.m4b
That naming may not be desirable for everyone, but it's an easy change to instead use the last of the combined chapter's title in the file name.
*/
private bool Step_DownloadAudiobookAsMultipleFilesPerChapter()
{
var zeroProgress = Step_DownloadAudiobook_Start();
var chapters = DownloadLicense.ChapterInfo.Chapters.ToList();
// Ensure split files are at least minChapterLength in duration.
var splitChapters = new ChapterInfo();
var runningTotal = TimeSpan.Zero;
string title = "";
for (int i = 0; i < chapters.Count; i++)
{
if (runningTotal == TimeSpan.Zero)
title = chapters[i].Title;
runningTotal += chapters[i].Duration;
if (runningTotal >= minChapterLength)
{
splitChapters.AddChapter(title, runningTotal);
runningTotal = TimeSpan.Zero;
}
}
// reset, just in case
multiPartFilePaths.Clear();
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
if (OutputFormat == OutputFormat.M4b)
ConvertToMultiMp4a(splitChapters);
else
ConvertToMultiMp3(splitChapters);
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
Step_DownloadAudiobook_End(zeroProgress);
var success = !IsCanceled;
if (success)
foreach (var path in multiPartFilePaths)
OnFileCreated(path);
return success;
}
private void ConvertToMultiMp4a(ChapterInfo splitChapters)
{
var chapterCount = 0;
AaxFile.ConvertToMultiMp4a(splitChapters, newSplitCallback =>
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback)
);
}
private void ConvertToMultiMp3(ChapterInfo splitChapters)
{
var chapterCount = 0;
AaxFile.ConvertToMultiMp3(splitChapters, newSplitCallback =>
{
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback);
newSplitCallback.LameConfig.ID3.Track = chapterCount.ToString();
});
}
private void createOutputFileStream(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
{
var fileName = FileUtility.GetMultipartFileName(OutputFileName, currentChapter, splitChapters.Count, newSplitCallback.Chapter.Title);
multiPartFilePaths.Add(fileName);
FileUtility.SaferDelete(fileName);
newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate);
}
}
}

View File

@ -0,0 +1,53 @@
using System;
using System.IO;
using AAXClean;
using Dinah.Core.StepRunner;
using FileManager;
namespace AaxDecrypter
{
public class AaxcDownloadSingleConverter : AaxcDownloadConvertBase
{
protected override StepSequence Steps { get; }
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
: base(outFileName, cacheDirectory, dlLic, outputFormat)
{
Steps = new StepSequence
{
Name = "Download and Convert Aaxc To " + OutputFormat,
["Step 1: Get Aaxc Metadata"] = Step_GetMetadata,
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsSingleFile,
["Step 3: Create Cue"] = Step_CreateCue,
["Step 4: Cleanup"] = Step_Cleanup,
};
}
private bool Step_DownloadAudiobookAsSingleFile()
{
var zeroProgress = Step_DownloadAudiobook_Start();
FileUtility.SaferDelete(OutputFileName);
var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
var decryptionResult
= OutputFormat == OutputFormat.M4b
? AaxFile.ConvertToMp4a(outputFile, DownloadLicense.ChapterInfo)
: AaxFile.ConvertToMp3(outputFile);
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
DownloadLicense.ChapterInfo = AaxFile.Chapters;
Step_DownloadAudiobook_End(zeroProgress);
var success = decryptionResult == ConversionResult.NoErrorsDetected && !IsCanceled;
if (success)
base.OnFileCreated(OutputFileName);
return success;
}
}
}

View File

@ -34,7 +34,7 @@ namespace AaxDecrypter
private string jsonDownloadState => Path.Combine(cacheDir, Path.ChangeExtension(OutputFileName, ".json")); private string jsonDownloadState => Path.Combine(cacheDir, Path.ChangeExtension(OutputFileName, ".json"));
private string tempFile => Path.ChangeExtension(jsonDownloadState, ".tmp"); private string tempFile => Path.ChangeExtension(jsonDownloadState, ".tmp");
public AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadLicense dlLic) protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadLicense dlLic)
{ {
OutputFileName = ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName)); OutputFileName = ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
@ -52,13 +52,10 @@ namespace AaxDecrypter
} }
public abstract void Cancel(); public abstract void Cancel();
protected abstract bool Step1_GetMetadata();
public virtual void SetCoverArt(byte[] coverArt) public virtual void SetCoverArt(byte[] coverArt)
{ {
if (coverArt is null) if (coverArt is not null)
return;
OnRetrievedCoverArt(coverArt); OnRetrievedCoverArt(coverArt);
} }
@ -93,7 +90,7 @@ namespace AaxDecrypter
nfsPersister?.Dispose(); nfsPersister?.Dispose();
} }
protected bool Step3_CreateCue() protected bool Step_CreateCue()
{ {
// not a critical step. its failure should not prevent future steps from running // not a critical step. its failure should not prevent future steps from running
try try
@ -105,12 +102,12 @@ namespace AaxDecrypter
} }
catch (Exception ex) catch (Exception ex)
{ {
Serilog.Log.Logger.Error(ex, $"{nameof(Step3_CreateCue)}. FAILED"); Serilog.Log.Logger.Error(ex, $"{nameof(Step_CreateCue)}. FAILED");
} }
return !IsCanceled; return !IsCanceled;
} }
protected bool Step4_Cleanup() protected bool Step_Cleanup()
{ {
FileUtility.SaferDelete(jsonDownloadState); FileUtility.SaferDelete(jsonDownloadState);
FileUtility.SaferDelete(tempFile); FileUtility.SaferDelete(tempFile);

View File

@ -17,10 +17,10 @@ namespace AaxDecrypter
{ {
Name = "Download Mp3 Audiobook", Name = "Download Mp3 Audiobook",
["Step 1: Get Mp3 Metadata"] = Step1_GetMetadata, ["Step 1: Get Mp3 Metadata"] = Step_GetMetadata,
["Step 2: Download Audiobook"] = Step2_DownloadAudiobookAsSingleFile, ["Step 2: Download Audiobook"] = Step_DownloadAudiobookAsSingleFile,
["Step 3: Create Cue"] = Step3_CreateCue, ["Step 3: Create Cue"] = Step_CreateCue,
["Step 4: Cleanup"] = Step4_Cleanup, ["Step 4: Cleanup"] = Step_Cleanup,
}; };
} }
@ -30,14 +30,14 @@ namespace AaxDecrypter
CloseInputFileStream(); CloseInputFileStream();
} }
protected override bool Step1_GetMetadata() protected bool Step_GetMetadata()
{ {
OnRetrievedCoverArt(null); OnRetrievedCoverArt(null);
return !IsCanceled; return !IsCanceled;
} }
private bool Step2_DownloadAudiobookAsSingleFile() private bool Step_DownloadAudiobookAsSingleFile()
{ {
DateTime startTime = DateTime.Now; DateTime startTime = DateTime.Now;

View File

@ -120,9 +120,10 @@ namespace FileLiberator
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory; var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
abDownloader = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm abDownloader
? new AaxcDownloadConverter(outFileName, cacheDir, audiobookDlLic, outputFormat, Configuration.Instance.SplitFilesByChapter) = contentLic.DrmType != AudibleApi.Common.DrmType.Adrm ? new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic)
: new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic); : Configuration.Instance.SplitFilesByChapter ? new AaxcDownloadMultiConverter(outFileName, cacheDir, audiobookDlLic, outputFormat)
: new AaxcDownloadSingleConverter(outFileName, cacheDir, audiobookDlLic, outputFormat);
abDownloader.DecryptProgressUpdate += (_, progress) => OnStreamingProgressChanged(progress); abDownloader.DecryptProgressUpdate += (_, progress) => OnStreamingProgressChanged(progress);
abDownloader.DecryptTimeRemaining += (_, remaining) => OnStreamingTimeRemaining(remaining); abDownloader.DecryptTimeRemaining += (_, remaining) => OnStreamingTimeRemaining(remaining);
abDownloader.RetrievedTitle += (_, title) => OnTitleDiscovered(title); abDownloader.RetrievedTitle += (_, title) => OnTitleDiscovered(title);
@ -166,11 +167,10 @@ namespace FileLiberator
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e) private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
{ {
if (e is null && Configuration.Instance.AllowLibationFixup)
OnRequestCoverArt(abDownloader.SetCoverArt);
if (e is not null) if (e is not null)
OnCoverImageDiscovered(e); OnCoverImageDiscovered(e);
else if (Configuration.Instance.AllowLibationFixup)
OnRequestCoverArt(abDownloader.SetCoverArt);
} }
/// <summary>Move new files to 'Books' directory</summary> /// <summary>Move new files to 'Books' directory</summary>