- Add more null safety - Fix possible FilePathCache race condition - Add MoveFilesToBooksDir progress reporting - All metadata is now downloaded in parallel with other post-success tasks. - Improve download resuming and file cleanup reliability - The downloader creates temp files with a UUID filename and does not insert them into the FilePathCache. Created files only receive their final file names when they are moved into the Books folder. This is to prepare for a future plan re naming templates
145 lines
5.5 KiB
C#
145 lines
5.5 KiB
C#
using AAXClean;
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
|
|
#nullable enable
|
|
namespace AaxDecrypter
|
|
{
|
|
public abstract class AaxcDownloadConvertBase : AudiobookDownloadBase
|
|
{
|
|
public event EventHandler<AppleTags>? RetrievedMetadata;
|
|
|
|
public Mp4File? AaxFile { get; private set; }
|
|
protected Mp4Operation? AaxConversion { get; set; }
|
|
|
|
protected AaxcDownloadConvertBase(string outDirectory, string cacheDirectory, IDownloadOptions dlOptions)
|
|
: base(outDirectory, cacheDirectory, dlOptions) { }
|
|
|
|
/// <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 is not null)
|
|
AaxFile.AppleTags.Cover = coverArt;
|
|
}
|
|
|
|
public override async Task CancelAsync()
|
|
{
|
|
await base.CancelAsync();
|
|
await (AaxConversion?.CancelAsync() ?? Task.CompletedTask);
|
|
}
|
|
|
|
private Mp4File Open()
|
|
{
|
|
if (DownloadOptions.DecryptionKeys is not KeyData[] keys || keys.Length == 0)
|
|
throw new InvalidOperationException($"{nameof(DownloadOptions.DecryptionKeys)} cannot be null or empty for a '{DownloadOptions.InputType}' file.");
|
|
else if (DownloadOptions.InputType is FileType.Dash)
|
|
{
|
|
//We may have multiple keys , so use the key whose key ID matches
|
|
//the dash files default Key ID.
|
|
var keyIds = keys.Select(k => new Guid(k.KeyPart1, bigEndian: true)).ToArray();
|
|
|
|
var dash = new DashFile(InputFileStream);
|
|
var kidIndex = Array.IndexOf(keyIds, dash.Tenc.DefaultKID);
|
|
|
|
if (kidIndex == -1)
|
|
throw new InvalidOperationException($"None of the {keyIds.Length} key IDs match the dash file's default KeyID of {dash.Tenc.DefaultKID}");
|
|
|
|
keys[0] = keys[kidIndex];
|
|
var keyId = keys[kidIndex].KeyPart1;
|
|
var key = keys[kidIndex].KeyPart2 ?? throw new InvalidOperationException($"{nameof(DownloadOptions.DecryptionKeys)} for '{DownloadOptions.InputType}' must have a non-null decryption key (KeyPart2).");
|
|
dash.SetDecryptionKey(keyId, key);
|
|
WriteKeyFile($"KeyId={Convert.ToHexString(keyId)}{Environment.NewLine}Key={Convert.ToHexString(key)}");
|
|
return dash;
|
|
}
|
|
else if (DownloadOptions.InputType is FileType.Aax)
|
|
{
|
|
var aax = new AaxFile(InputFileStream);
|
|
var key = keys[0].KeyPart1;
|
|
aax.SetDecryptionKey(keys[0].KeyPart1);
|
|
WriteKeyFile($"ActivationBytes={Convert.ToHexString(key)}");
|
|
return aax;
|
|
}
|
|
else if (DownloadOptions.InputType is FileType.Aaxc)
|
|
{
|
|
var aax = new AaxFile(InputFileStream);
|
|
var key = keys[0].KeyPart1;
|
|
var iv = keys[0].KeyPart2 ?? throw new InvalidOperationException($"{nameof(DownloadOptions.DecryptionKeys)} for '{DownloadOptions.InputType}' must have a non-null initialization vector (KeyPart2).");
|
|
aax.SetDecryptionKey(keys[0].KeyPart1, iv);
|
|
WriteKeyFile($"Key={Convert.ToHexString(key)}{Environment.NewLine}IV={Convert.ToHexString(iv)}");
|
|
return aax;
|
|
}
|
|
else throw new InvalidOperationException($"{nameof(DownloadOptions.InputType)} of '{DownloadOptions.InputType}' is unknown.");
|
|
|
|
void WriteKeyFile(string contents)
|
|
{
|
|
var keyFile = Path.Combine(Path.ChangeExtension(InputFileStream.SaveFilePath, ".key"));
|
|
File.WriteAllText(keyFile, contents + Environment.NewLine);
|
|
OnTempFileCreated(new(keyFile));
|
|
}
|
|
}
|
|
|
|
protected bool Step_GetMetadata()
|
|
{
|
|
AaxFile = Open();
|
|
|
|
RetrievedMetadata?.Invoke(this, AaxFile.AppleTags);
|
|
|
|
if (DownloadOptions.StripUnabridged)
|
|
{
|
|
AaxFile.AppleTags.Title = AaxFile.AppleTags.TitleSansUnabridged;
|
|
AaxFile.AppleTags.Album = AaxFile.AppleTags.Album?.Replace(" (Unabridged)", "");
|
|
}
|
|
|
|
if (DownloadOptions.FixupFile)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Narrator))
|
|
AaxFile.AppleTags.AppleListBox.EditOrAddTag("©wrt", AaxFile.AppleTags.Narrator);
|
|
|
|
if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Copyright))
|
|
AaxFile.AppleTags.Copyright = AaxFile.AppleTags.Copyright.Replace("(P)", "℗").Replace("©", "©");
|
|
|
|
//Add audiobook shelf tags
|
|
//https://github.com/advplyr/audiobookshelf/issues/1794#issuecomment-1565050213
|
|
const string tagDomain = "com.pilabor.tone";
|
|
|
|
AaxFile.AppleTags.Title = DownloadOptions.Title;
|
|
|
|
if (DownloadOptions.Subtitle is string subtitle)
|
|
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "SUBTITLE", subtitle);
|
|
|
|
if (DownloadOptions.Publisher is string publisher)
|
|
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PUBLISHER", publisher);
|
|
|
|
if (DownloadOptions.Language is string language)
|
|
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "LANGUAGE", language);
|
|
|
|
if (DownloadOptions.AudibleProductId is string asin)
|
|
{
|
|
AaxFile.AppleTags.Asin = asin;
|
|
AaxFile.AppleTags.AppleListBox.EditOrAddTag("asin", asin);
|
|
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_ASIN", asin);
|
|
}
|
|
|
|
if (DownloadOptions.SeriesName is string series)
|
|
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "SERIES", series);
|
|
|
|
if (DownloadOptions.SeriesNumber is float part)
|
|
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PART", part.ToString());
|
|
}
|
|
|
|
OnInitialized();
|
|
OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged);
|
|
OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor);
|
|
OnRetrievedNarrators(AaxFile.AppleTags.Narrator);
|
|
OnRetrievedCoverArt(AaxFile.AppleTags.Cover);
|
|
|
|
return !IsCanceled;
|
|
}
|
|
|
|
protected virtual void OnInitialized() { }
|
|
}
|
|
}
|