Fix occasional error of audio downloads hanging.
This commit is contained in:
parent
9366b3baca
commit
10c01f4147
@ -1,5 +1,6 @@
|
|||||||
using AAXClean;
|
using AAXClean;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace AaxDecrypter
|
namespace AaxDecrypter
|
||||||
@ -33,20 +34,33 @@ namespace AaxDecrypter
|
|||||||
{
|
{
|
||||||
if (DownloadOptions.InputType is FileType.Dash)
|
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 = DownloadOptions.DecryptionKeys.Select(k => new Guid(k.KeyPart1, bigEndian: true)).ToArray();
|
||||||
|
|
||||||
var dash = new DashFile(InputFileStream);
|
var dash = new DashFile(InputFileStream);
|
||||||
dash.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
|
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}");
|
||||||
|
|
||||||
|
DownloadOptions.DecryptionKeys[0] = DownloadOptions.DecryptionKeys[kidIndex];
|
||||||
|
var keyId = DownloadOptions.DecryptionKeys[kidIndex].KeyPart1;
|
||||||
|
var key = DownloadOptions.DecryptionKeys[kidIndex].KeyPart2;
|
||||||
|
|
||||||
|
dash.SetDecryptionKey(keyId, key);
|
||||||
return dash;
|
return dash;
|
||||||
}
|
}
|
||||||
else if (DownloadOptions.InputType is FileType.Aax)
|
else if (DownloadOptions.InputType is FileType.Aax)
|
||||||
{
|
{
|
||||||
var aax = new AaxFile(InputFileStream);
|
var aax = new AaxFile(InputFileStream);
|
||||||
aax.SetDecryptionKey(DownloadOptions.AudibleKey);
|
aax.SetDecryptionKey(DownloadOptions.DecryptionKeys[0].KeyPart1);
|
||||||
return aax;
|
return aax;
|
||||||
}
|
}
|
||||||
else if (DownloadOptions.InputType is FileType.Aaxc)
|
else if (DownloadOptions.InputType is FileType.Aaxc)
|
||||||
{
|
{
|
||||||
var aax = new AaxFile(InputFileStream);
|
var aax = new AaxFile(InputFileStream);
|
||||||
aax.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
|
aax.SetDecryptionKey(DownloadOptions.DecryptionKeys[0].KeyPart1, DownloadOptions.DecryptionKeys[0].KeyPart2);
|
||||||
return aax;
|
return aax;
|
||||||
}
|
}
|
||||||
else throw new InvalidOperationException($"{nameof(DownloadOptions.InputType)} of '{DownloadOptions.InputType}' is unknown.");
|
else throw new InvalidOperationException($"{nameof(DownloadOptions.InputType)} of '{DownloadOptions.InputType}' is unknown.");
|
||||||
|
|||||||
@ -73,11 +73,16 @@ namespace AaxDecrypter
|
|||||||
AsyncSteps[$"Cleanup"] = CleanupAsync;
|
AsyncSteps[$"Cleanup"] = CleanupAsync;
|
||||||
(bool success, var elapsed) = await AsyncSteps.RunAsync();
|
(bool success, var elapsed) = await AsyncSteps.RunAsync();
|
||||||
|
|
||||||
|
//Stop the downloader so it doesn't keep running in the background.
|
||||||
|
if (!success)
|
||||||
|
nfsPersister.Dispose();
|
||||||
|
|
||||||
await progressTask;
|
await progressTask;
|
||||||
|
|
||||||
var speedup = DownloadOptions.RuntimeLength / elapsed;
|
var speedup = DownloadOptions.RuntimeLength / elapsed;
|
||||||
Serilog.Log.Information($"Speedup is {speedup:F0}x realtime.");
|
Serilog.Log.Information($"Speedup is {speedup:F0}x realtime.");
|
||||||
|
|
||||||
|
nfsPersister.Dispose();
|
||||||
return success;
|
return success;
|
||||||
|
|
||||||
async Task reportProgress()
|
async Task reportProgress()
|
||||||
@ -177,7 +182,7 @@ namespace AaxDecrypter
|
|||||||
|
|
||||||
FileUtility.SaferDelete(jsonDownloadState);
|
FileUtility.SaferDelete(jsonDownloadState);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(DownloadOptions.AudibleKey) &&
|
if (DownloadOptions.DecryptionKeys != null &&
|
||||||
DownloadOptions.RetainEncryptedFile &&
|
DownloadOptions.RetainEncryptedFile &&
|
||||||
DownloadOptions.InputType is AAXClean.FileType fileType)
|
DownloadOptions.InputType is AAXClean.FileType fileType)
|
||||||
{
|
{
|
||||||
@ -188,17 +193,21 @@ namespace AaxDecrypter
|
|||||||
|
|
||||||
if (fileType is AAXClean.FileType.Aax)
|
if (fileType is AAXClean.FileType.Aax)
|
||||||
{
|
{
|
||||||
await File.WriteAllTextAsync(keyPath, $"ActivationBytes={DownloadOptions.AudibleKey}");
|
await File.WriteAllTextAsync(keyPath, $"ActivationBytes={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart1)}");
|
||||||
aaxPath = Path.ChangeExtension(tempFilePath, ".aax");
|
aaxPath = Path.ChangeExtension(tempFilePath, ".aax");
|
||||||
}
|
}
|
||||||
else if (fileType is AAXClean.FileType.Aaxc)
|
else if (fileType is AAXClean.FileType.Aaxc)
|
||||||
{
|
{
|
||||||
await File.WriteAllTextAsync(keyPath, $"Key={DownloadOptions.AudibleKey}{Environment.NewLine}IV={DownloadOptions.AudibleIV}");
|
await File.WriteAllTextAsync(keyPath,
|
||||||
|
$"Key={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart1)}{Environment.NewLine}" +
|
||||||
|
$"IV={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart2)}");
|
||||||
aaxPath = Path.ChangeExtension(tempFilePath, ".aaxc");
|
aaxPath = Path.ChangeExtension(tempFilePath, ".aaxc");
|
||||||
}
|
}
|
||||||
else if (fileType is AAXClean.FileType.Dash)
|
else if (fileType is AAXClean.FileType.Dash)
|
||||||
{
|
{
|
||||||
await File.WriteAllTextAsync(keyPath, $"KeyId={DownloadOptions.AudibleKey}{Environment.NewLine}Key={DownloadOptions.AudibleIV}");
|
await File.WriteAllTextAsync(keyPath,
|
||||||
|
$"KeyId={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart1)}{Environment.NewLine}" +
|
||||||
|
$"Key={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart2)}");
|
||||||
aaxPath = Path.ChangeExtension(tempFilePath, ".dash");
|
aaxPath = Path.ChangeExtension(tempFilePath, ".dash");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@ -2,15 +2,35 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace AaxDecrypter
|
namespace AaxDecrypter
|
||||||
{
|
{
|
||||||
public interface IDownloadOptions
|
public class KeyData
|
||||||
|
{
|
||||||
|
public byte[] KeyPart1 { get; }
|
||||||
|
public byte[]? KeyPart2 { get; }
|
||||||
|
|
||||||
|
public KeyData(byte[] keyPart1, byte[]? keyPart2 = null)
|
||||||
|
{
|
||||||
|
KeyPart1 = keyPart1;
|
||||||
|
KeyPart2 = keyPart2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyData(string keyPart1, string? keyPart2 = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(keyPart1, nameof(keyPart1));
|
||||||
|
KeyPart1 = Convert.FromBase64String(keyPart1);
|
||||||
|
if (keyPart2 != null)
|
||||||
|
KeyPart2 = Convert.FromBase64String(keyPart2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IDownloadOptions
|
||||||
{
|
{
|
||||||
event EventHandler<long> DownloadSpeedChanged;
|
event EventHandler<long> DownloadSpeedChanged;
|
||||||
string DownloadUrl { get; }
|
string DownloadUrl { get; }
|
||||||
string UserAgent { get; }
|
string UserAgent { get; }
|
||||||
string AudibleKey { get; }
|
KeyData[]? DecryptionKeys { get; }
|
||||||
string AudibleIV { get; }
|
|
||||||
TimeSpan RuntimeLength { get; }
|
TimeSpan RuntimeLength { get; }
|
||||||
OutputFormat OutputFormat { get; }
|
OutputFormat OutputFormat { get; }
|
||||||
bool TrimOutputToChapterLength { get; }
|
bool TrimOutputToChapterLength { get; }
|
||||||
@ -21,14 +41,14 @@ namespace AaxDecrypter
|
|||||||
long DownloadSpeedBps { get; }
|
long DownloadSpeedBps { get; }
|
||||||
ChapterInfo ChapterInfo { get; }
|
ChapterInfo ChapterInfo { get; }
|
||||||
bool FixupFile { get; }
|
bool FixupFile { get; }
|
||||||
string AudibleProductId { get; }
|
string? AudibleProductId { get; }
|
||||||
string Title { get; }
|
string? Title { get; }
|
||||||
string Subtitle { get; }
|
string? Subtitle { get; }
|
||||||
string Publisher { get; }
|
string? Publisher { get; }
|
||||||
string Language { get; }
|
string? Language { get; }
|
||||||
string SeriesName { get; }
|
string? SeriesName { get; }
|
||||||
float? SeriesNumber { get; }
|
float? SeriesNumber { get; }
|
||||||
NAudio.Lame.LameConfig LameConfig { get; }
|
NAudio.Lame.LameConfig? LameConfig { get; }
|
||||||
bool Downsample { get; }
|
bool Downsample { get; }
|
||||||
bool MatchSourceBitrate { get; }
|
bool MatchSourceBitrate { get; }
|
||||||
bool MoveMoovToBeginning { get; }
|
bool MoveMoovToBeginning { get; }
|
||||||
|
|||||||
@ -110,14 +110,16 @@ namespace AaxDecrypter
|
|||||||
#region Downloader
|
#region Downloader
|
||||||
|
|
||||||
/// <summary> Update the <see cref="Dinah.Core.IO.JsonFilePersister{T}"/>. </summary>
|
/// <summary> Update the <see cref="Dinah.Core.IO.JsonFilePersister{T}"/>. </summary>
|
||||||
private void OnUpdate()
|
private void OnUpdate(bool waitForWrite = false)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (DateTime.UtcNow > NextUpdateTime)
|
if (waitForWrite || DateTime.UtcNow > NextUpdateTime)
|
||||||
{
|
{
|
||||||
Updated?.Invoke(this, EventArgs.Empty);
|
Updated?.Invoke(this, EventArgs.Empty);
|
||||||
//JsonFilePersister Will not allow update intervals shorter than 100 milliseconds
|
//JsonFilePersister Will not allow update intervals shorter than 100 milliseconds
|
||||||
|
//If an update is called less than 100 ms since the last update, persister will
|
||||||
|
//sleep the thread until 100 ms has elapsed.
|
||||||
NextUpdateTime = DateTime.UtcNow.AddMilliseconds(110);
|
NextUpdateTime = DateTime.UtcNow.AddMilliseconds(110);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -305,7 +307,7 @@ namespace AaxDecrypter
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_downloadedPiece.Set();
|
_downloadedPiece.Set();
|
||||||
OnUpdate();
|
OnUpdate(waitForWrite: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,7 +404,7 @@ namespace AaxDecrypter
|
|||||||
_cancellationSource?.Dispose();
|
_cancellationSource?.Dispose();
|
||||||
_readFile.Dispose();
|
_readFile.Dispose();
|
||||||
_writeFile.Dispose();
|
_writeFile.Dispose();
|
||||||
OnUpdate();
|
OnUpdate(waitForWrite: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
|||||||
@ -55,7 +55,7 @@ public class WidevineKey
|
|||||||
Type = (KeyType)type;
|
Type = (KeyType)type;
|
||||||
Key = key;
|
Key = key;
|
||||||
}
|
}
|
||||||
public override string ToString() => $"{Convert.ToHexString(Kid.ToByteArray()).ToLower()}:{Convert.ToHexString(Key).ToLower()}";
|
public override string ToString() => $"{Convert.ToHexString(Kid.ToByteArray(bigEndian: true)).ToLower()}:{Convert.ToHexString(Key).ToLower()}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class Cdm
|
public partial class Cdm
|
||||||
@ -192,7 +192,7 @@ public partial class Cdm
|
|||||||
id = id.Append(new byte[16 - id.Length]);
|
id = id.Append(new byte[16 - id.Length]);
|
||||||
}
|
}
|
||||||
|
|
||||||
keys[i] = new WidevineKey(new Guid(id), keyContainer.Type, keyBytes);
|
keys[i] = new WidevineKey(new Guid(id,bigEndian: true), keyContainer.Type, keyBytes);
|
||||||
}
|
}
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,12 +41,31 @@ public partial class DownloadOptions
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<ContentLicense> ChooseContent(Api api, LibraryBook libraryBook, Configuration config)
|
private class LicenseInfo
|
||||||
|
{
|
||||||
|
public DrmType DrmType { get; }
|
||||||
|
public ContentMetadata ContentMetadata { get; set; }
|
||||||
|
public KeyData[]? DecryptionKeys { get; }
|
||||||
|
public LicenseInfo(ContentLicense license, IEnumerable<KeyData>? keys = null)
|
||||||
|
{
|
||||||
|
DrmType = license.DrmType;
|
||||||
|
ContentMetadata = license.ContentMetadata;
|
||||||
|
DecryptionKeys = keys?.ToArray() ?? ToKeys(license.Voucher);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KeyData[]? ToKeys(VoucherDtoV10? voucher)
|
||||||
|
=> voucher is null ? null : [new KeyData(voucher.Key, voucher.Iv)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<LicenseInfo> ChooseContent(Api api, LibraryBook libraryBook, Configuration config)
|
||||||
{
|
{
|
||||||
var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High;
|
var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High;
|
||||||
|
|
||||||
if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm)
|
if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm)
|
||||||
return await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
|
{
|
||||||
|
var license = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
|
||||||
|
return new LicenseInfo(license);
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -62,6 +81,9 @@ public partial class DownloadOptions
|
|||||||
config.RequestSpatial,
|
config.RequestSpatial,
|
||||||
codecChoice);
|
codecChoice);
|
||||||
|
|
||||||
|
if (contentLic.DrmType is not DrmType.Widevine)
|
||||||
|
return new LicenseInfo(contentLic);
|
||||||
|
|
||||||
using var client = new HttpClient();
|
using var client = new HttpClient();
|
||||||
using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse);
|
using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse);
|
||||||
var dash = new MpegDash(mpdResponse.Content.ReadAsStream());
|
var dash = new MpegDash(mpdResponse.Content.ReadAsStream());
|
||||||
@ -75,12 +97,7 @@ public partial class DownloadOptions
|
|||||||
var challenge = session.GetLicenseChallenge(dash);
|
var challenge = session.GetLicenseChallenge(dash);
|
||||||
var licenseMessage = await api.WidevineDrmLicense(libraryBook.Book.AudibleProductId, challenge);
|
var licenseMessage = await api.WidevineDrmLicense(libraryBook.Book.AudibleProductId, challenge);
|
||||||
var keys = session.ParseLicense(licenseMessage);
|
var keys = session.ParseLicense(licenseMessage);
|
||||||
contentLic.Voucher = new VoucherDtoV10()
|
return new LicenseInfo(contentLic, keys.Select(k => new KeyData(k.Kid.ToByteArray(bigEndian: true), k.Key)));
|
||||||
{
|
|
||||||
Key = Convert.ToHexStringLower(keys[0].Kid.ToByteArray()),
|
|
||||||
Iv = Convert.ToHexStringLower(keys[0].Key)
|
|
||||||
};
|
|
||||||
return contentLic;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -93,41 +110,20 @@ public partial class DownloadOptions
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, ContentLicense contentLic)
|
private static DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, LicenseInfo licInfo)
|
||||||
{
|
{
|
||||||
//If DrmType is not Adrm or Widevine, the delivered file is an unencrypted mp3.
|
|
||||||
var outputFormat
|
|
||||||
= contentLic.DrmType is not DrmType.Adrm and not DrmType.Widevine ||
|
|
||||||
(config.AllowLibationFixup && config.DecryptToLossy && contentLic.ContentMetadata.ContentReference.Codec != "ac-4")
|
|
||||||
? OutputFormat.Mp3
|
|
||||||
: OutputFormat.M4b;
|
|
||||||
|
|
||||||
long chapterStartMs
|
long chapterStartMs
|
||||||
= config.StripAudibleBrandAudio
|
= config.StripAudibleBrandAudio
|
||||||
? contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs
|
? licInfo.ContentMetadata.ChapterInfo.BrandIntroDurationMs
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
AAXClean.FileType? inputType
|
var dlOptions = new DownloadOptions(config, libraryBook, licInfo)
|
||||||
= contentLic.DrmType is DrmType.Widevine ? AAXClean.FileType.Dash
|
|
||||||
: contentLic.DrmType is DrmType.Adrm && contentLic.Voucher?.Key.Length == 8 && contentLic.Voucher?.Iv == null ? AAXClean.FileType.Aax
|
|
||||||
: contentLic.DrmType is DrmType.Adrm && contentLic.Voucher?.Key.Length == 32 && contentLic.Voucher?.Iv.Length == 32 ? AAXClean.FileType.Aaxc
|
|
||||||
: null;
|
|
||||||
|
|
||||||
var dlOptions = new DownloadOptions(config, libraryBook, contentLic.ContentMetadata.ContentUrl?.OfflineUrl)
|
|
||||||
{
|
{
|
||||||
AudibleKey = contentLic.Voucher?.Key,
|
|
||||||
AudibleIV = contentLic.Voucher?.Iv,
|
|
||||||
InputType = inputType,
|
|
||||||
OutputFormat = outputFormat,
|
|
||||||
DrmType = contentLic.DrmType,
|
|
||||||
ContentMetadata = contentLic.ContentMetadata,
|
|
||||||
LameConfig = outputFormat == OutputFormat.Mp3 ? GetLameOptions(config) : null,
|
|
||||||
ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)),
|
ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)),
|
||||||
RuntimeLength = TimeSpan.FromMilliseconds(contentLic.ContentMetadata.ChapterInfo.RuntimeLengthMs),
|
RuntimeLength = TimeSpan.FromMilliseconds(licInfo.ContentMetadata.ChapterInfo.RuntimeLengthMs),
|
||||||
};
|
};
|
||||||
|
|
||||||
dlOptions.LibraryBookDto.Codec = contentLic.ContentMetadata.ContentReference.Codec;
|
if (TryGetAudioInfo(licInfo.ContentMetadata.ContentUrl, out int? bitrate, out int? sampleRate, out int? channels))
|
||||||
if (TryGetAudioInfo(contentLic.ContentMetadata.ContentUrl, out int? bitrate, out int? sampleRate, out int? channels))
|
|
||||||
{
|
{
|
||||||
dlOptions.LibraryBookDto.BitRate = bitrate;
|
dlOptions.LibraryBookDto.BitRate = bitrate;
|
||||||
dlOptions.LibraryBookDto.SampleRate = sampleRate;
|
dlOptions.LibraryBookDto.SampleRate = sampleRate;
|
||||||
@ -136,7 +132,7 @@ public partial class DownloadOptions
|
|||||||
|
|
||||||
var titleConcat = config.CombineNestedChapterTitles ? ": " : null;
|
var titleConcat = config.CombineNestedChapterTitles ? ": " : null;
|
||||||
var chapters
|
var chapters
|
||||||
= flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters, titleConcat)
|
= flattenChapters(licInfo.ContentMetadata.ChapterInfo.Chapters, titleConcat)
|
||||||
.OrderBy(c => c.StartOffsetMs)
|
.OrderBy(c => c.StartOffsetMs)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@ -152,7 +148,7 @@ public partial class DownloadOptions
|
|||||||
chapLenMs -= chapterStartMs;
|
chapLenMs -= chapterStartMs;
|
||||||
|
|
||||||
if (config.StripAudibleBrandAudio && i == chapters.Count - 1)
|
if (config.StripAudibleBrandAudio && i == chapters.Count - 1)
|
||||||
chapLenMs -= contentLic.ContentMetadata.ChapterInfo.BrandOutroDurationMs;
|
chapLenMs -= licInfo.ContentMetadata.ChapterInfo.BrandOutroDurationMs;
|
||||||
|
|
||||||
dlOptions.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs));
|
dlOptions.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,27 +9,27 @@ using System.IO;
|
|||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using LibationFileManager.Templates;
|
using LibationFileManager.Templates;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace FileLiberator
|
namespace FileLiberator
|
||||||
{
|
{
|
||||||
public partial class DownloadOptions : IDownloadOptions, IDisposable
|
public partial class DownloadOptions : IDownloadOptions, IDisposable
|
||||||
{
|
{
|
||||||
public event EventHandler<long> DownloadSpeedChanged;
|
public event EventHandler<long>? DownloadSpeedChanged;
|
||||||
public LibraryBook LibraryBook { get; }
|
public LibraryBook LibraryBook { get; }
|
||||||
public LibraryBookDto LibraryBookDto { get; }
|
public LibraryBookDto LibraryBookDto { get; }
|
||||||
public string DownloadUrl { get; }
|
public string DownloadUrl { get; }
|
||||||
public string AudibleKey { get; init; }
|
public KeyData[]? DecryptionKeys { get; }
|
||||||
public string AudibleIV { get; init; }
|
public required TimeSpan RuntimeLength { get; init; }
|
||||||
public TimeSpan RuntimeLength { get; init; }
|
public OutputFormat OutputFormat { get; }
|
||||||
public OutputFormat OutputFormat { get; init; }
|
public required ChapterInfo ChapterInfo { get; init; }
|
||||||
public ChapterInfo ChapterInfo { get; init; }
|
|
||||||
public string Title => LibraryBook.Book.Title;
|
public string Title => LibraryBook.Book.Title;
|
||||||
public string Subtitle => LibraryBook.Book.Subtitle;
|
public string Subtitle => LibraryBook.Book.Subtitle;
|
||||||
public string Publisher => LibraryBook.Book.Publisher;
|
public string Publisher => LibraryBook.Book.Publisher;
|
||||||
public string Language => LibraryBook.Book.Language;
|
public string Language => LibraryBook.Book.Language;
|
||||||
public string AudibleProductId => LibraryBookDto.AudibleProductId;
|
public string? AudibleProductId => LibraryBookDto.AudibleProductId;
|
||||||
public string SeriesName => LibraryBookDto.FirstSeries?.Name;
|
public string? SeriesName => LibraryBookDto.FirstSeries?.Name;
|
||||||
public float? SeriesNumber => LibraryBookDto.FirstSeries?.Number;
|
public float? SeriesNumber => LibraryBookDto.FirstSeries?.Number;
|
||||||
public NAudio.Lame.LameConfig LameConfig { get; init; }
|
public NAudio.Lame.LameConfig? LameConfig { get; }
|
||||||
public string UserAgent => AudibleApi.Resources.Download_User_Agent;
|
public string UserAgent => AudibleApi.Resources.Download_User_Agent;
|
||||||
public bool TrimOutputToChapterLength => Config.AllowLibationFixup && Config.StripAudibleBrandAudio;
|
public bool TrimOutputToChapterLength => Config.AllowLibationFixup && Config.StripAudibleBrandAudio;
|
||||||
public bool StripUnabridged => Config.AllowLibationFixup && Config.StripUnabridged;
|
public bool StripUnabridged => Config.AllowLibationFixup && Config.StripUnabridged;
|
||||||
@ -41,15 +41,15 @@ namespace FileLiberator
|
|||||||
public bool Downsample => Config.AllowLibationFixup && Config.LameDownsampleMono;
|
public bool Downsample => Config.AllowLibationFixup && Config.LameDownsampleMono;
|
||||||
public bool MatchSourceBitrate => Config.AllowLibationFixup && Config.LameMatchSourceBR && Config.LameTargetBitrate;
|
public bool MatchSourceBitrate => Config.AllowLibationFixup && Config.LameMatchSourceBR && Config.LameTargetBitrate;
|
||||||
public bool MoveMoovToBeginning => Config.MoveMoovToBeginning;
|
public bool MoveMoovToBeginning => Config.MoveMoovToBeginning;
|
||||||
public AAXClean.FileType? InputType { get; init; }
|
public AAXClean.FileType? InputType { get; }
|
||||||
public AudibleApi.Common.DrmType DrmType { get; init; }
|
public AudibleApi.Common.DrmType DrmType { get; }
|
||||||
public AudibleApi.Common.ContentMetadata ContentMetadata { get; init; }
|
public AudibleApi.Common.ContentMetadata ContentMetadata { get; }
|
||||||
|
|
||||||
public string GetMultipartFileName(MultiConvertFileProperties props)
|
public string GetMultipartFileName(MultiConvertFileProperties props)
|
||||||
{
|
{
|
||||||
var baseDir = Path.GetDirectoryName(props.OutputFileName);
|
var baseDir = Path.GetDirectoryName(props.OutputFileName);
|
||||||
var extension = Path.GetExtension(props.OutputFileName);
|
var extension = Path.GetExtension(props.OutputFileName);
|
||||||
return Templates.ChapterFile.GetFilename(LibraryBookDto, props, baseDir, extension);
|
return Templates.ChapterFile.GetFilename(LibraryBookDto, props, baseDir!, extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetMultipartTitle(MultiConvertFileProperties props)
|
public string GetMultipartTitle(MultiConvertFileProperties props)
|
||||||
@ -92,14 +92,38 @@ namespace FileLiberator
|
|||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DownloadOptions(Configuration config, LibraryBook libraryBook, [System.Diagnostics.CodeAnalysis.NotNull] string downloadUrl)
|
private DownloadOptions(Configuration config, LibraryBook libraryBook, LicenseInfo licInfo)
|
||||||
{
|
{
|
||||||
Config = ArgumentValidator.EnsureNotNull(config, nameof(config));
|
Config = ArgumentValidator.EnsureNotNull(config, nameof(config));
|
||||||
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
|
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
|
||||||
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
|
|
||||||
// no null/empty check for key/iv. unencrypted files do not have them
|
|
||||||
|
|
||||||
|
ArgumentValidator.EnsureNotNull(licInfo, nameof(licInfo));
|
||||||
|
|
||||||
|
if (licInfo.ContentMetadata.ContentUrl.OfflineUrl is not string licUrl)
|
||||||
|
throw new InvalidDataException("Content license doesn't contain an offline Url");
|
||||||
|
|
||||||
|
DownloadUrl = licUrl;
|
||||||
|
DecryptionKeys = licInfo.DecryptionKeys;
|
||||||
|
DrmType = licInfo.DrmType;
|
||||||
|
ContentMetadata = licInfo.ContentMetadata;
|
||||||
|
InputType
|
||||||
|
= licInfo.DrmType is AudibleApi.Common.DrmType.Widevine ? AAXClean.FileType.Dash
|
||||||
|
: licInfo.DrmType is AudibleApi.Common.DrmType.Adrm && licInfo.DecryptionKeys?.Length == 1 && licInfo.DecryptionKeys[0].KeyPart1.Length == 8 && licInfo.DecryptionKeys[0].KeyPart2 is null ? AAXClean.FileType.Aax
|
||||||
|
: licInfo.DrmType is AudibleApi.Common.DrmType.Adrm && licInfo.DecryptionKeys?.Length == 1 && licInfo.DecryptionKeys[0].KeyPart1.Length == 32 && licInfo.DecryptionKeys[0].KeyPart2?.Length == 32 ? AAXClean.FileType.Aaxc
|
||||||
|
: null;
|
||||||
|
|
||||||
|
//If DrmType is not Adrm or Widevine, the delivered file is an unencrypted mp3.
|
||||||
|
OutputFormat
|
||||||
|
= licInfo.DrmType is not AudibleApi.Common.DrmType.Adrm and not AudibleApi.Common.DrmType.Widevine ||
|
||||||
|
(config.AllowLibationFixup && config.DecryptToLossy && licInfo.ContentMetadata.ContentReference.Codec != Ac4Codec)
|
||||||
|
? OutputFormat.Mp3
|
||||||
|
: OutputFormat.M4b;
|
||||||
|
|
||||||
|
LameConfig = OutputFormat == OutputFormat.Mp3 ? GetLameOptions(config) : null;
|
||||||
|
|
||||||
|
// no null/empty check for key/iv. unencrypted files do not have them
|
||||||
LibraryBookDto = LibraryBook.ToDto();
|
LibraryBookDto = LibraryBook.ToDto();
|
||||||
|
LibraryBookDto.Codec = licInfo.ContentMetadata.ContentReference.Codec;
|
||||||
|
|
||||||
cancellation =
|
cancellation =
|
||||||
config
|
config
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user