Libation/AaxDecrypter/AaxcDownloadConverter.cs
2021-07-03 18:59:18 -06:00

296 lines
11 KiB
C#

using Dinah.Core;
using Dinah.Core.Diagnostics;
using Dinah.Core.IO;
using Dinah.Core.StepRunner;
using System;
using System.IO;
namespace AaxDecrypter
{
public interface ISimpleAaxcToM4bConverter
{
event EventHandler<AaxcTagLibFile> RetrievedTags;
event EventHandler<byte[]> RetrievedCoverArt;
event EventHandler<TimeSpan> DecryptTimeRemaining;
event EventHandler<int> DecryptProgressUpdate;
bool Run();
string AppName { get; set; }
string outDir { get; }
string outputFileName { get; }
DownloadLicense downloadLicense { get; }
AaxcTagLibFile aaxcTagLib { get; }
byte[] coverArt { get; }
void SetCoverArt(byte[] coverArt);
void SetOutputFilename(string outFileName);
}
public interface IAdvancedAaxcToM4bConverter : ISimpleAaxcToM4bConverter
{
void Cancel();
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<byte[]> RetrievedCoverArt;
public event EventHandler<int> DecryptProgressUpdate;
public event EventHandler<TimeSpan> DecryptTimeRemaining;
public string AppName { get; set; } = nameof(AaxcDownloadConverter);
public string outDir { get; private set; }
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 byte[] coverArt { get; private set; }
private StepSequence steps { get; }
private FFMpegAaxcProcesser aaxcProcesser;
private bool isCanceled { get; set; }
private string jsonDownloadState => Path.Combine(cacheDir, Path.GetFileNameWithoutExtension(outputFileName) + ".json");
private string tempFile => PathLib.ReplaceExtension(jsonDownloadState, ".aaxc");
public static AaxcDownloadConverter Create(string cacheDirectory, string outDirectory, DownloadLicense dlLic)
{
var converter = new AaxcDownloadConverter(cacheDirectory, outDirectory, dlLic);
converter.SetOutputFilename(Path.GetTempFileName());
return converter;
}
private AaxcDownloadConverter(string cacheDirectory, string outDirectory, DownloadLicense dlLic)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(outDirectory, nameof(outDirectory));
ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
if (!Directory.Exists(outDirectory))
throw new ArgumentNullException(nameof(cacheDirectory), "Directory does not exist");
if (!Directory.Exists(outDirectory))
throw new ArgumentNullException(nameof(outDirectory), "Directory does not exist");
cacheDir = cacheDirectory;
outDir = outDirectory;
steps = new StepSequence
{
Name = "Download and Convert Aaxc To M4b",
["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;
}
public void SetOutputFilename(string outFileName)
{
outputFileName = PathLib.ReplaceExtension(outFileName, ".m4b");
outDir = Path.GetDirectoryName(outputFileName);
if (File.Exists(outputFileName))
File.Delete(outputFileName);
}
public void SetCoverArt(byte[] coverArt)
{
if (coverArt is null) return;
this.coverArt = coverArt;
RetrievedCoverArt?.Invoke(this, coverArt);
}
public bool Run()
{
var (IsSuccess, Elapsed) = steps.Run();
if (!IsSuccess)
{
Console.WriteLine("WARNING-Conversion failed");
return false;
}
var speedup = (int)(aaxcTagLib.Properties.Duration.TotalSeconds / (long)Elapsed.TotalSeconds);
Console.WriteLine("Speedup is " + speedup + "x realtime.");
Console.WriteLine("Done");
return true;
}
public bool Step1_CreateDir()
{
ProcessRunner.WorkingDir = outDir;
Directory.CreateDirectory(outDir);
return !isCanceled;
}
public bool Step2_GetMetadata()
{
//Get metadata from the file over http
NetworkFileStreamPersister nfsPersister;
if (File.Exists(jsonDownloadState))
{
nfsPersister = new NetworkFileStreamPersister(jsonDownloadState);
}
else
{
var headers = new System.Net.WebHeaderCollection();
headers.Add("User-Agent", downloadLicense.UserAgent);
NetworkFileStream networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
nfsPersister = new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
}
var networkFile = new NetworkFileAbstraction(nfsPersister.NetworkFileStream);
aaxcTagLib = new AaxcTagLibFile(networkFile);
nfsPersister.Dispose();
if (coverArt is null && aaxcTagLib.AppleTags.Pictures.Length > 0)
{
coverArt = aaxcTagLib.AppleTags.Pictures[0].Data.Data;
}
RetrievedTags?.Invoke(this, aaxcTagLib);
RetrievedCoverArt?.Invoke(this, coverArt);
return !isCanceled;
}
public bool Step3_DownloadAndCombine()
{
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);
NetworkFileStream networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
nfsPersister = new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
}
string metadataPath = Path.Combine(outDir, Path.GetFileName(outputFileName) + ".ffmeta");
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);
}
//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;
}
private void AaxcProcesser_ProgressUpdate(object sender, AaxcProcessUpdate e)
{
double remainingSecsToProcess = (aaxcTagLib.Properties.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;
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
{
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), downloadLicense.ChapterInfo));
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"{nameof(Step5_CreateCue)}. FAILED");
}
return !isCanceled;
}
public bool Step6_CreateNfo()
{
try
{
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxcTagLib, downloadLicense.ChapterInfo));
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"{nameof(Step6_CreateNfo)}. FAILED");
}
return !isCanceled;
}
public bool Step7_Cleanup()
{
FileExt.SafeDelete(jsonDownloadState);
FileExt.SafeDelete(tempFile);
return !isCanceled;
}
public void Cancel()
{
isCanceled = true;
aaxcProcesser.Cancel();
}
}
}