Merge pull request #1250 from Mbucari/master
Bug fixes and a change to license request logic
This commit is contained in:
commit
7f1b357c52
@ -1,5 +1,6 @@
|
||||
using AAXClean;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AaxDecrypter
|
||||
@ -8,7 +9,7 @@ namespace AaxDecrypter
|
||||
{
|
||||
public event EventHandler<AppleTags> RetrievedMetadata;
|
||||
|
||||
protected Mp4File AaxFile { get; private set; }
|
||||
public Mp4File AaxFile { get; private set; }
|
||||
protected Mp4Operation AaxConversion { get; set; }
|
||||
|
||||
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
||||
@ -33,20 +34,33 @@ namespace AaxDecrypter
|
||||
{
|
||||
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);
|
||||
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;
|
||||
}
|
||||
else if (DownloadOptions.InputType is FileType.Aax)
|
||||
{
|
||||
var aax = new AaxFile(InputFileStream);
|
||||
aax.SetDecryptionKey(DownloadOptions.AudibleKey);
|
||||
aax.SetDecryptionKey(DownloadOptions.DecryptionKeys[0].KeyPart1);
|
||||
return aax;
|
||||
}
|
||||
else if (DownloadOptions.InputType is FileType.Aaxc)
|
||||
{
|
||||
var aax = new AaxFile(InputFileStream);
|
||||
aax.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
|
||||
aax.SetDecryptionKey(DownloadOptions.DecryptionKeys[0].KeyPart1, DownloadOptions.DecryptionKeys[0].KeyPart2);
|
||||
return aax;
|
||||
}
|
||||
else throw new InvalidOperationException($"{nameof(DownloadOptions.InputType)} of '{DownloadOptions.InputType}' is unknown.");
|
||||
|
||||
@ -73,11 +73,16 @@ namespace AaxDecrypter
|
||||
AsyncSteps[$"Cleanup"] = CleanupAsync;
|
||||
(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;
|
||||
|
||||
var speedup = DownloadOptions.RuntimeLength / elapsed;
|
||||
Serilog.Log.Information($"Speedup is {speedup:F0}x realtime.");
|
||||
|
||||
nfsPersister.Dispose();
|
||||
return success;
|
||||
|
||||
async Task reportProgress()
|
||||
@ -177,7 +182,7 @@ namespace AaxDecrypter
|
||||
|
||||
FileUtility.SaferDelete(jsonDownloadState);
|
||||
|
||||
if (!string.IsNullOrEmpty(DownloadOptions.AudibleKey) &&
|
||||
if (DownloadOptions.DecryptionKeys != null &&
|
||||
DownloadOptions.RetainEncryptedFile &&
|
||||
DownloadOptions.InputType is AAXClean.FileType fileType)
|
||||
{
|
||||
@ -188,22 +193,29 @@ namespace AaxDecrypter
|
||||
|
||||
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");
|
||||
}
|
||||
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");
|
||||
}
|
||||
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");
|
||||
}
|
||||
else
|
||||
throw new InvalidOperationException($"Unknown file type: {fileType}");
|
||||
|
||||
if (tempFilePath != aaxPath)
|
||||
FileUtility.SaferMove(tempFilePath, aaxPath);
|
||||
|
||||
OnFileCreated(aaxPath);
|
||||
OnFileCreated(keyPath);
|
||||
}
|
||||
|
||||
@ -2,15 +2,35 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
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;
|
||||
string DownloadUrl { get; }
|
||||
string UserAgent { get; }
|
||||
string AudibleKey { get; }
|
||||
string AudibleIV { get; }
|
||||
KeyData[]? DecryptionKeys { get; }
|
||||
TimeSpan RuntimeLength { get; }
|
||||
OutputFormat OutputFormat { get; }
|
||||
bool TrimOutputToChapterLength { get; }
|
||||
@ -21,14 +41,14 @@ namespace AaxDecrypter
|
||||
long DownloadSpeedBps { get; }
|
||||
ChapterInfo ChapterInfo { get; }
|
||||
bool FixupFile { get; }
|
||||
string AudibleProductId { get; }
|
||||
string Title { get; }
|
||||
string Subtitle { get; }
|
||||
string Publisher { get; }
|
||||
string Language { get; }
|
||||
string SeriesName { get; }
|
||||
string? AudibleProductId { get; }
|
||||
string? Title { get; }
|
||||
string? Subtitle { get; }
|
||||
string? Publisher { get; }
|
||||
string? Language { get; }
|
||||
string? SeriesName { get; }
|
||||
float? SeriesNumber { get; }
|
||||
NAudio.Lame.LameConfig LameConfig { get; }
|
||||
NAudio.Lame.LameConfig? LameConfig { get; }
|
||||
bool Downsample { get; }
|
||||
bool MatchSourceBitrate { get; }
|
||||
bool MoveMoovToBeginning { get; }
|
||||
|
||||
@ -110,14 +110,16 @@ namespace AaxDecrypter
|
||||
#region Downloader
|
||||
|
||||
/// <summary> Update the <see cref="Dinah.Core.IO.JsonFilePersister{T}"/>. </summary>
|
||||
private void OnUpdate()
|
||||
private void OnUpdate(bool waitForWrite = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (DateTime.UtcNow > NextUpdateTime)
|
||||
if (waitForWrite || DateTime.UtcNow > NextUpdateTime)
|
||||
{
|
||||
Updated?.Invoke(this, EventArgs.Empty);
|
||||
//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);
|
||||
}
|
||||
}
|
||||
@ -305,7 +307,7 @@ namespace AaxDecrypter
|
||||
finally
|
||||
{
|
||||
_downloadedPiece.Set();
|
||||
OnUpdate();
|
||||
OnUpdate(waitForWrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -402,7 +404,7 @@ namespace AaxDecrypter
|
||||
_cancellationSource?.Dispose();
|
||||
_readFile.Dispose();
|
||||
_writeFile.Dispose();
|
||||
OnUpdate();
|
||||
OnUpdate(waitForWrite: true);
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
|
||||
@ -32,7 +32,7 @@ namespace ApplicationServices
|
||||
ScanEnd += (_, __) => Scanning = false;
|
||||
}
|
||||
|
||||
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, Task<ApiExtended>> apiExtendedfunc, IEnumerable<LibraryBook> existingLibrary, params Account[] accounts)
|
||||
public static async Task<List<LibraryBook>> FindInactiveBooks(IEnumerable<LibraryBook> existingLibrary, params Account[] accounts)
|
||||
{
|
||||
logRestart();
|
||||
|
||||
@ -58,7 +58,7 @@ namespace ApplicationServices
|
||||
try
|
||||
{
|
||||
logTime($"pre {nameof(scanAccountsAsync)} all");
|
||||
var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
|
||||
var libraryItems = await scanAccountsAsync(accounts, libraryOptions);
|
||||
logTime($"post {nameof(scanAccountsAsync)} all");
|
||||
|
||||
var totalCount = libraryItems.Count;
|
||||
@ -101,7 +101,7 @@ namespace ApplicationServices
|
||||
}
|
||||
|
||||
#region FULL LIBRARY scan and import
|
||||
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[]? accounts)
|
||||
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(params Account[]? accounts)
|
||||
{
|
||||
logRestart();
|
||||
|
||||
@ -131,7 +131,7 @@ namespace ApplicationServices
|
||||
| LibraryOptions.ResponseGroupOptions.IsFinished,
|
||||
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215
|
||||
};
|
||||
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
|
||||
var importItems = await scanAccountsAsync(accounts, libraryOptions);
|
||||
logTime($"post {nameof(scanAccountsAsync)} all");
|
||||
|
||||
var totalCount = importItems.Count;
|
||||
@ -262,7 +262,7 @@ namespace ApplicationServices
|
||||
return null;
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
|
||||
private static async Task<List<ImportItem>> scanAccountsAsync(Account[] accounts, LibraryOptions libraryOptions)
|
||||
{
|
||||
var tasks = new List<Task<List<ImportItem>>>();
|
||||
|
||||
@ -278,7 +278,7 @@ namespace ApplicationServices
|
||||
try
|
||||
{
|
||||
// get APIs in serial b/c of logins. do NOT move inside of parallel (Task.WhenAll)
|
||||
var apiExtended = await apiExtendedfunc(account);
|
||||
var apiExtended = await ApiExtended.CreateAsync(account);
|
||||
|
||||
// add scanAccountAsync as a TASK: do not await
|
||||
tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions, archiver));
|
||||
|
||||
@ -11,11 +11,13 @@ using Polly;
|
||||
using Polly.Retry;
|
||||
using System.Threading;
|
||||
|
||||
#nullable enable
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
|
||||
public class ApiExtended
|
||||
{
|
||||
public static Func<Account, ILoginChoiceEager>? LoginChoiceFactory { get; set; }
|
||||
public Api Api { get; private set; }
|
||||
|
||||
private const int MaxConcurrency = 10;
|
||||
@ -24,52 +26,46 @@ namespace AudibleUtilities
|
||||
private ApiExtended(Api api) => Api = api;
|
||||
|
||||
/// <summary>Get api from existing tokens else login with 'eager' choice. External browser url is provided. Response can be external browser login or continuing with native api callbacks.</summary>
|
||||
public static async Task<ApiExtended> CreateAsync(Account account, ILoginChoiceEager loginChoiceEager)
|
||||
{
|
||||
Serilog.Log.Logger.Information("{@DebugInfo}", new
|
||||
{
|
||||
LoginType = nameof(ILoginChoiceEager),
|
||||
Account = account?.MaskedLogEntry ?? "[null]",
|
||||
LocaleName = account?.Locale?.Name
|
||||
});
|
||||
|
||||
var api = await EzApiCreator.GetApiAsync(
|
||||
loginChoiceEager,
|
||||
account.Locale,
|
||||
AudibleApiStorage.AccountsSettingsFile,
|
||||
account.GetIdentityTokensJsonPath());
|
||||
return new ApiExtended(api);
|
||||
}
|
||||
|
||||
/// <summary>Get api from existing tokens. Assumes you have valid login tokens. Else exception</summary>
|
||||
public static async Task<ApiExtended> CreateAsync(Account account)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
ArgumentValidator.EnsureNotNull(account.AccountId, nameof(account.AccountId));
|
||||
ArgumentValidator.EnsureNotNull(account.Locale, nameof(account.Locale));
|
||||
|
||||
try
|
||||
{
|
||||
Serilog.Log.Logger.Information("{@DebugInfo}", new
|
||||
{
|
||||
AccountMaskedLogEntry = account.MaskedLogEntry
|
||||
});
|
||||
|
||||
return await CreateAsync(account.AccountId, account.Locale.Name);
|
||||
var api = await EzApiCreator.GetApiAsync(
|
||||
account.Locale,
|
||||
AudibleApiStorage.AccountsSettingsFile,
|
||||
account.GetIdentityTokensJsonPath());
|
||||
return new ApiExtended(api);
|
||||
}
|
||||
|
||||
/// <summary>Get api from existing tokens. Assumes you have valid login tokens. Else exception</summary>
|
||||
public static async Task<ApiExtended> CreateAsync(string username, string localeName)
|
||||
catch
|
||||
{
|
||||
if (LoginChoiceFactory is null)
|
||||
throw new InvalidOperationException($"The UI module must first set {LoginChoiceFactory} before attempting to create the api");
|
||||
|
||||
Serilog.Log.Logger.Information("{@DebugInfo}", new
|
||||
{
|
||||
Username = username.ToMask(),
|
||||
LocaleName = localeName,
|
||||
LoginType = nameof(ILoginChoiceEager),
|
||||
Account = account.MaskedLogEntry ?? "[null]",
|
||||
LocaleName = account.Locale?.Name
|
||||
});
|
||||
|
||||
var api = await EzApiCreator.GetApiAsync(
|
||||
Localization.Get(localeName),
|
||||
LoginChoiceFactory(account),
|
||||
account.Locale,
|
||||
AudibleApiStorage.AccountsSettingsFile,
|
||||
AudibleApiStorage.GetIdentityTokensJsonPath(username, localeName));
|
||||
account.GetIdentityTokensJsonPath());
|
||||
|
||||
return new ApiExtended(api);
|
||||
}
|
||||
}
|
||||
|
||||
private static AsyncRetryPolicy policy { get; }
|
||||
= Policy.Handle<Exception>()
|
||||
|
||||
@ -55,7 +55,7 @@ public class WidevineKey
|
||||
Type = (KeyType)type;
|
||||
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
|
||||
@ -192,7 +192,7 @@ public partial class Cdm
|
||||
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;
|
||||
}
|
||||
|
||||
@ -171,6 +171,29 @@ namespace FileLiberator
|
||||
if (sender is not AaxcDownloadConvertBase converter || converter.DownloadOptions is not DownloadOptions options)
|
||||
return;
|
||||
|
||||
#region Prevent erroneous truncation due to incorrect chapter info
|
||||
|
||||
//Sometimes the chapter info is not accurate. Since AAXClean trims audio
|
||||
//files to the chapters start and end, if the last chapter's end time is
|
||||
//before the end of the audio file, the file will be truncated to match
|
||||
//the chapter. This is never desirable, so pad the last chapter to match
|
||||
//the original audio length.
|
||||
|
||||
var fileDuration = converter.AaxFile.Duration;
|
||||
if (options.Config.StripAudibleBrandAudio)
|
||||
fileDuration -= TimeSpan.FromMilliseconds(options.ContentMetadata.ChapterInfo.BrandOutroDurationMs);
|
||||
|
||||
var durationDelta = fileDuration - options.ChapterInfo.EndOffset;
|
||||
//Remove the last chapter and re-add it with the durationDelta that will
|
||||
//make the chapter's end coincide with the end of the audio file.
|
||||
var chapters = options.ChapterInfo.Chapters as List<AAXClean.Chapter>;
|
||||
var lastChapter = chapters[^1];
|
||||
|
||||
chapters.Remove(lastChapter);
|
||||
options.ChapterInfo.Add(lastChapter.Title, lastChapter.Duration + durationDelta);
|
||||
|
||||
#endregion
|
||||
|
||||
tags.Title ??= options.LibraryBookDto.TitleWithSubtitle;
|
||||
tags.Album ??= tags.Title;
|
||||
tags.Artist ??= string.Join("; ", options.LibraryBook.Book.Authors.Select(a => a.Name));
|
||||
|
||||
@ -41,26 +41,38 @@ public partial class DownloadOptions
|
||||
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;
|
||||
|
||||
if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm)
|
||||
return await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
|
||||
|
||||
ContentLicense? contentLic = null, fallback = null;
|
||||
{
|
||||
var license = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
|
||||
return new LicenseInfo(license);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
//try to request a widevine content license using the user's spatial audio settings
|
||||
var codecChoice = config.SpatialAudioCodec switch
|
||||
{
|
||||
Configuration.SpatialCodec.EC_3 => Ec3Codec,
|
||||
Configuration.SpatialCodec.AC_4 => Ac4Codec,
|
||||
_ => throw new NotSupportedException($"Unknown value for {nameof(config.SpatialAudioCodec)}")
|
||||
};
|
||||
var codecChoice = config.SpatialAudioCodec is Configuration.SpatialCodec.AC_4 ? Ac4Codec : Ec3Codec;
|
||||
|
||||
contentLic
|
||||
var contentLic
|
||||
= await api.GetDownloadLicenseAsync(
|
||||
libraryBook.Book.AudibleProductId,
|
||||
dlQuality,
|
||||
@ -68,51 +80,10 @@ public partial class DownloadOptions
|
||||
DrmType.Widevine,
|
||||
config.RequestSpatial,
|
||||
codecChoice);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Failed to request a Widevine license.");
|
||||
//We failed to get a widevine license, so fall back to AAX(C)
|
||||
return await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
|
||||
}
|
||||
|
||||
if (!contentLic.ContentMetadata.ContentReference.IsSpatial && contentLic.DrmType != DrmType.Adrm)
|
||||
{
|
||||
/*
|
||||
We got a widevine license and we have a Cdm, but we still need to decide if we WANT the file
|
||||
being delivered with widevine. This file is not "spatial", so it may be no better than the
|
||||
audio in the Adrm files. All else being equal, we prefer Adrm files because they have more
|
||||
build-in metadata and always AAC-LC, which is a codec playable by pretty much every device
|
||||
in existence.
|
||||
if (contentLic.DrmType is not DrmType.Widevine)
|
||||
return new LicenseInfo(contentLic);
|
||||
|
||||
Unfortunately, there appears to be no way to determine which codec/quality combination we'll
|
||||
get until we make the request and see what content gets delivered. For some books,
|
||||
Widevine/High delivers 44.1 kHz / 128 kbps audio and Adrm/High delivers 22.05 kHz / 64 kbps.
|
||||
In those cases, the Widevine content size is much larger. Other books will deliver the same
|
||||
sample rate / bitrate for both Widevine and Adrm, the only difference being codec. Widevine
|
||||
is usually xHE-AAC, but is sometimes AAC-LC. Adrm is always AAC-LC.
|
||||
|
||||
To decide which file we want, use this simple rule: if files are different codecs and
|
||||
Widevine is significantly larger, use Widevine. Otherwise use ADRM.
|
||||
*/
|
||||
|
||||
fallback = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
|
||||
|
||||
var wvCr = contentLic.ContentMetadata.ContentReference;
|
||||
var adrmCr = fallback.ContentMetadata.ContentReference;
|
||||
|
||||
if (wvCr.Codec == adrmCr.Codec ||
|
||||
adrmCr.ContentSizeInBytes > wvCr.ContentSizeInBytes ||
|
||||
RelativePercentDifference(adrmCr.ContentSizeInBytes, wvCr.ContentSizeInBytes) < 0.05)
|
||||
{
|
||||
contentLic = fallback;
|
||||
}
|
||||
}
|
||||
|
||||
if (contentLic.DrmType == DrmType.Widevine)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse);
|
||||
var dash = new MpegDash(mpdResponse.Content.ReadAsStream());
|
||||
@ -126,61 +97,33 @@ public partial class DownloadOptions
|
||||
var challenge = session.GetLicenseChallenge(dash);
|
||||
var licenseMessage = await api.WidevineDrmLicense(libraryBook.Book.AudibleProductId, challenge);
|
||||
var keys = session.ParseLicense(licenseMessage);
|
||||
contentLic.Voucher = new VoucherDtoV10()
|
||||
{
|
||||
Key = Convert.ToHexStringLower(keys[0].Kid.ToByteArray()),
|
||||
Iv = Convert.ToHexStringLower(keys[0].Key)
|
||||
};
|
||||
return new LicenseInfo(contentLic, keys.Select(k => new KeyData(k.Kid.ToByteArray(bigEndian: true), k.Key)));
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (fallback != null)
|
||||
return fallback;
|
||||
|
||||
//We won't have a fallback if the requested license is for a spatial audio file.
|
||||
//Throw so that the user is aware that spatial audio exists and that they were not able to download it.
|
||||
Serilog.Log.Logger.Error(ex, "Failed to request a Widevine license.");
|
||||
//We failed to get a widevine content license. Depending on the
|
||||
//failure reason, users can potentially still download this audiobook
|
||||
//by disabling the "Use Widevine DRM" feature.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
return contentLic;
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
= config.StripAudibleBrandAudio
|
||||
? contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs
|
||||
? licInfo.ContentMetadata.ChapterInfo.BrandIntroDurationMs
|
||||
: 0;
|
||||
|
||||
AAXClean.FileType? inputType
|
||||
= 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)
|
||||
var dlOptions = new DownloadOptions(config, libraryBook, licInfo)
|
||||
{
|
||||
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)),
|
||||
RuntimeLength = TimeSpan.FromMilliseconds(contentLic.ContentMetadata.ChapterInfo.RuntimeLengthMs),
|
||||
RuntimeLength = TimeSpan.FromMilliseconds(licInfo.ContentMetadata.ChapterInfo.RuntimeLengthMs),
|
||||
};
|
||||
|
||||
dlOptions.LibraryBookDto.Codec = contentLic.ContentMetadata.ContentReference.Codec;
|
||||
if (TryGetAudioInfo(contentLic.ContentMetadata.ContentUrl, out int? bitrate, out int? sampleRate, out int? channels))
|
||||
if (TryGetAudioInfo(licInfo.ContentMetadata.ContentUrl, out int? bitrate, out int? sampleRate, out int? channels))
|
||||
{
|
||||
dlOptions.LibraryBookDto.BitRate = bitrate;
|
||||
dlOptions.LibraryBookDto.SampleRate = sampleRate;
|
||||
@ -189,7 +132,7 @@ public partial class DownloadOptions
|
||||
|
||||
var titleConcat = config.CombineNestedChapterTitles ? ": " : null;
|
||||
var chapters
|
||||
= flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters, titleConcat)
|
||||
= flattenChapters(licInfo.ContentMetadata.ChapterInfo.Chapters, titleConcat)
|
||||
.OrderBy(c => c.StartOffsetMs)
|
||||
.ToList();
|
||||
|
||||
@ -205,7 +148,7 @@ public partial class DownloadOptions
|
||||
chapLenMs -= chapterStartMs;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@ -9,47 +9,47 @@ using System.IO;
|
||||
using ApplicationServices;
|
||||
using LibationFileManager.Templates;
|
||||
|
||||
#nullable enable
|
||||
namespace FileLiberator
|
||||
{
|
||||
public partial class DownloadOptions : IDownloadOptions, IDisposable
|
||||
{
|
||||
public event EventHandler<long> DownloadSpeedChanged;
|
||||
public event EventHandler<long>? DownloadSpeedChanged;
|
||||
public LibraryBook LibraryBook { get; }
|
||||
public LibraryBookDto LibraryBookDto { get; }
|
||||
public string DownloadUrl { get; }
|
||||
public string AudibleKey { get; init; }
|
||||
public string AudibleIV { get; init; }
|
||||
public TimeSpan RuntimeLength { get; init; }
|
||||
public OutputFormat OutputFormat { get; init; }
|
||||
public ChapterInfo ChapterInfo { get; init; }
|
||||
public KeyData[]? DecryptionKeys { get; }
|
||||
public required TimeSpan RuntimeLength { get; init; }
|
||||
public OutputFormat OutputFormat { get; }
|
||||
public required ChapterInfo ChapterInfo { get; init; }
|
||||
public string Title => LibraryBook.Book.Title;
|
||||
public string Subtitle => LibraryBook.Book.Subtitle;
|
||||
public string Publisher => LibraryBook.Book.Publisher;
|
||||
public string Language => LibraryBook.Book.Language;
|
||||
public string AudibleProductId => LibraryBookDto.AudibleProductId;
|
||||
public string SeriesName => LibraryBookDto.FirstSeries?.Name;
|
||||
public string? AudibleProductId => LibraryBookDto.AudibleProductId;
|
||||
public string? SeriesName => LibraryBookDto.FirstSeries?.Name;
|
||||
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 bool TrimOutputToChapterLength => config.AllowLibationFixup && config.StripAudibleBrandAudio;
|
||||
public bool StripUnabridged => config.AllowLibationFixup && config.StripUnabridged;
|
||||
public bool CreateCueSheet => config.CreateCueSheet;
|
||||
public bool DownloadClipsBookmarks => config.DownloadClipsBookmarks;
|
||||
public long DownloadSpeedBps => config.DownloadSpeedLimit;
|
||||
public bool RetainEncryptedFile => config.RetainAaxFile;
|
||||
public bool FixupFile => config.AllowLibationFixup;
|
||||
public bool Downsample => config.AllowLibationFixup && config.LameDownsampleMono;
|
||||
public bool MatchSourceBitrate => config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate;
|
||||
public bool MoveMoovToBeginning => config.MoveMoovToBeginning;
|
||||
public AAXClean.FileType? InputType { get; init; }
|
||||
public AudibleApi.Common.DrmType DrmType { get; init; }
|
||||
public AudibleApi.Common.ContentMetadata ContentMetadata { get; init; }
|
||||
public bool TrimOutputToChapterLength => Config.AllowLibationFixup && Config.StripAudibleBrandAudio;
|
||||
public bool StripUnabridged => Config.AllowLibationFixup && Config.StripUnabridged;
|
||||
public bool CreateCueSheet => Config.CreateCueSheet;
|
||||
public bool DownloadClipsBookmarks => Config.DownloadClipsBookmarks;
|
||||
public long DownloadSpeedBps => Config.DownloadSpeedLimit;
|
||||
public bool RetainEncryptedFile => Config.RetainAaxFile;
|
||||
public bool FixupFile => Config.AllowLibationFixup;
|
||||
public bool Downsample => Config.AllowLibationFixup && Config.LameDownsampleMono;
|
||||
public bool MatchSourceBitrate => Config.AllowLibationFixup && Config.LameMatchSourceBR && Config.LameTargetBitrate;
|
||||
public bool MoveMoovToBeginning => Config.MoveMoovToBeginning;
|
||||
public AAXClean.FileType? InputType { get; }
|
||||
public AudibleApi.Common.DrmType DrmType { get; }
|
||||
public AudibleApi.Common.ContentMetadata ContentMetadata { get; }
|
||||
|
||||
public string GetMultipartFileName(MultiConvertFileProperties props)
|
||||
{
|
||||
var baseDir = Path.GetDirectoryName(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)
|
||||
@ -59,7 +59,7 @@ namespace FileLiberator
|
||||
{
|
||||
if (DownloadClipsBookmarks)
|
||||
{
|
||||
var format = config.ClipsBookmarksFileFormat;
|
||||
var format = Config.ClipsBookmarksFileFormat;
|
||||
|
||||
var formatExtension = format.ToString().ToLowerInvariant();
|
||||
var filePath = Path.ChangeExtension(fileName, formatExtension);
|
||||
@ -84,7 +84,7 @@ namespace FileLiberator
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private readonly Configuration config;
|
||||
public Configuration Config { get; }
|
||||
private readonly IDisposable cancellation;
|
||||
public void Dispose()
|
||||
{
|
||||
@ -92,14 +92,38 @@ namespace FileLiberator
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private DownloadOptions(Configuration config, LibraryBook libraryBook, [System.Diagnostics.CodeAnalysis.NotNull] string downloadUrl)
|
||||
private DownloadOptions(Configuration config, LibraryBook libraryBook, LicenseInfo licInfo)
|
||||
{
|
||||
this.config = ArgumentValidator.EnsureNotNull(config, nameof(config));
|
||||
Config = ArgumentValidator.EnsureNotNull(config, nameof(config));
|
||||
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.Codec = licInfo.ContentMetadata.ContentReference.Codec;
|
||||
|
||||
cancellation =
|
||||
config
|
||||
|
||||
@ -20,9 +20,15 @@ namespace FileLiberator
|
||||
account: libraryBook.Account.ToMask()
|
||||
);
|
||||
|
||||
public static Func<Account, Task<ApiExtended>>? ApiExtendedFunc { get; set; }
|
||||
|
||||
public static async Task<AudibleApi.Api> GetApiAsync(this LibraryBook libraryBook)
|
||||
{
|
||||
var apiExtended = await ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale);
|
||||
Account account;
|
||||
using (var accounts = AudibleApiStorage.GetAccountsSettingsPersister())
|
||||
account = accounts.AccountsSettings.GetAccount(libraryBook.Account, libraryBook.Book.Locale);
|
||||
|
||||
var apiExtended = await ApiExtended.CreateAsync(account);
|
||||
return apiExtended.Api;
|
||||
}
|
||||
|
||||
|
||||
@ -31,10 +31,29 @@ namespace LibationAvalonia.Controls.Settings
|
||||
if (!accounts.AccountsSettings.Accounts.Any(a => a.IdentityTokens.DeviceType == AudibleApi.Resources.DeviceType))
|
||||
{
|
||||
if (VisualRoot is Window parent)
|
||||
await MessageBox.Show(parent,
|
||||
"Your must remove account(s) from Libation and then re-add them to enable widwvine content.",
|
||||
{
|
||||
var choice = await MessageBox.Show(parent,
|
||||
"In order to enable widevine content, Libation will need to log into your accounts again.\r\n\r\n" +
|
||||
"Do you want Libation to clear your current account settings and prompt you to login before the next download?",
|
||||
"Widevine Content Unavailable",
|
||||
MessageBoxButtons.OK);
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question,
|
||||
MessageBoxDefaultButton.Button2);
|
||||
|
||||
if (choice == DialogResult.Yes)
|
||||
{
|
||||
foreach (var account in accounts.AccountsSettings.Accounts.ToArray())
|
||||
{
|
||||
if (account.IdentityTokens.DeviceType != AudibleApi.Resources.DeviceType)
|
||||
{
|
||||
accounts.AccountsSettings.Delete(account);
|
||||
var acc = accounts.AccountsSettings.Upsert(account.AccountId, account.Locale.Name);
|
||||
acc.AccountName = account.AccountName;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_viewModel.UseWidevine = false;
|
||||
}
|
||||
|
||||
@ -9,10 +9,6 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
public class AvaloniaLoginChoiceEager : ILoginChoiceEager
|
||||
{
|
||||
/// <summary>Convenience method. Recommended when wiring up Winforms to <see cref="ApplicationServices.LibraryCommands.ImportAccountAsync"/></summary>
|
||||
public static async Task<ApiExtended> ApiExtendedFunc(Account account)
|
||||
=> await ApiExtended.CreateAsync(account, new AvaloniaLoginChoiceEager(account));
|
||||
|
||||
public ILoginCallback LoginCallback { get; }
|
||||
|
||||
private readonly Account _account;
|
||||
|
||||
@ -201,7 +201,7 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
try
|
||||
{
|
||||
var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(LibationAvalonia.Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, accounts);
|
||||
var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(accounts);
|
||||
|
||||
// this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop
|
||||
if (Configuration.Instance.ShowImportedStats && newAdded > 0)
|
||||
|
||||
@ -27,7 +27,7 @@ namespace LibationAvalonia.ViewModels
|
||||
// in autoScan, new books SHALL NOT show dialog
|
||||
try
|
||||
{
|
||||
await LibraryCommands.ImportAccountAsync(LibationAvalonia.Dialogs.Login.AvaloniaLoginChoiceEager.ApiExtendedFunc, accounts);
|
||||
await LibraryCommands.ImportAccountAsync(accounts);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
@ -431,7 +431,7 @@ namespace LibationAvalonia.ViewModels
|
||||
.Select(lbe => lbe.LibraryBook)
|
||||
.Where(lb => !lb.Book.HasLiberated());
|
||||
|
||||
var removedBooks = await LibraryCommands.FindInactiveBooks(AvaloniaLoginChoiceEager.ApiExtendedFunc, lib, accounts);
|
||||
var removedBooks = await LibraryCommands.FindInactiveBooks(lib, accounts);
|
||||
|
||||
var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ namespace LibationAvalonia.Views
|
||||
public MainWindow()
|
||||
{
|
||||
DataContext = new MainVM(this);
|
||||
ApiExtended.LoginChoiceFactory = account => new Dialogs.Login.AvaloniaLoginChoiceEager(account);
|
||||
|
||||
AudibleApiStorage.LoadError += AudibleApiStorage_LoadError;
|
||||
InitializeComponent();
|
||||
|
||||
@ -32,7 +32,7 @@ namespace LibationCli
|
||||
: $"Scanning Audible library: {_accounts.Length} accounts. This may take a few minutes per account.";
|
||||
Console.WriteLine(intro);
|
||||
|
||||
var (TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportAccountAsync((a) => ApiExtended.CreateAsync(a), _accounts);
|
||||
var (TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportAccountAsync(_accounts);
|
||||
|
||||
Console.WriteLine("Scan complete.");
|
||||
Console.WriteLine($"Total processed: {TotalBooksProcessed}");
|
||||
|
||||
@ -257,7 +257,7 @@ namespace LibationFileManager
|
||||
}
|
||||
|
||||
[Description("Use widevine DRM")]
|
||||
public bool UseWidevine { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
public bool UseWidevine { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Request Spatial Audio")]
|
||||
public bool RequestSpatial { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
|
||||
@ -41,7 +41,8 @@ public class LibationContributor
|
||||
GitHubUser("muchtall"),
|
||||
GitHubUser("ScubyG"),
|
||||
GitHubUser("patienttruth"),
|
||||
GitHubUser("stickystyle")
|
||||
GitHubUser("stickystyle"),
|
||||
GitHubUser("cherez"),
|
||||
]);
|
||||
|
||||
private LibationContributor(string name, LibationContributorType type,Uri link)
|
||||
|
||||
@ -9,17 +9,11 @@ namespace LibationWinForms.Login
|
||||
{
|
||||
public class WinformLoginChoiceEager : WinformLoginBase, ILoginChoiceEager
|
||||
{
|
||||
/// <summary>Convenience method. Recommended when wiring up Winforms to <see cref="ApplicationServices.LibraryCommands.ImportAccountAsync"/></summary>
|
||||
public static Func<Account, Task<ApiExtended>> CreateApiExtendedFunc(IWin32Window owner) => a => ApiExtendedFunc(a, owner);
|
||||
|
||||
private static async Task<ApiExtended> ApiExtendedFunc(Account account, IWin32Window owner)
|
||||
=> await ApiExtended.CreateAsync(account, new WinformLoginChoiceEager(account, owner));
|
||||
|
||||
public ILoginCallback LoginCallback { get; private set; }
|
||||
|
||||
private Account _account { get; }
|
||||
|
||||
private WinformLoginChoiceEager(Account account, IWin32Window owner) : base(owner)
|
||||
public WinformLoginChoiceEager(Account account, IWin32Window owner) : base(owner)
|
||||
{
|
||||
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
LoginCallback = new WinformLoginCallback(_account, owner);
|
||||
|
||||
@ -206,10 +206,28 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
if (!accounts.AccountsSettings.Accounts.Any(a => a.IdentityTokens.DeviceType == AudibleApi.Resources.DeviceType))
|
||||
{
|
||||
MessageBox.Show(this,
|
||||
"Your must remove account(s) from Libation and then re-add them to enable widwvine content.",
|
||||
var choice = MessageBox.Show(this,
|
||||
"In order to enable widevine content, Libation will need to log into your accounts again.\r\n\r\n" +
|
||||
"Do you want Libation to clear your current account settings and prompt you to login before the next download?",
|
||||
"Widevine Content Unavailable",
|
||||
MessageBoxButtons.OK);
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question,
|
||||
MessageBoxDefaultButton.Button2);
|
||||
|
||||
if (choice == DialogResult.Yes)
|
||||
{
|
||||
foreach (var account in accounts.AccountsSettings.Accounts.ToArray())
|
||||
{
|
||||
if (account.IdentityTokens.DeviceType != AudibleApi.Resources.DeviceType)
|
||||
{
|
||||
accounts.AccountsSettings.Delete(account);
|
||||
var acc = accounts.AccountsSettings.Upsert(account.AccountId, account.Locale.Name);
|
||||
acc.AccountName = account.AccountName;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
useWidevineCbox.Checked = false;
|
||||
return;
|
||||
|
||||
@ -32,7 +32,7 @@ namespace LibationWinForms
|
||||
// in autoScan, new books SHALL NOT show dialog
|
||||
try
|
||||
{
|
||||
Task importAsync() => LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.CreateApiExtendedFunc(this), accounts);
|
||||
Task importAsync() => LibraryCommands.ImportAccountAsync(accounts);
|
||||
if (InvokeRequired)
|
||||
await Invoke(importAsync);
|
||||
else
|
||||
|
||||
@ -74,7 +74,7 @@ namespace LibationWinForms
|
||||
{
|
||||
try
|
||||
{
|
||||
var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.CreateApiExtendedFunc(this), accounts);
|
||||
var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(accounts);
|
||||
|
||||
// this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop
|
||||
if (Configuration.Instance.ShowImportedStats && newAdded > 0)
|
||||
|
||||
@ -4,8 +4,11 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.Login;
|
||||
using Octokit;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
@ -56,6 +59,7 @@ namespace LibationWinForms
|
||||
=> Invoke(() => productsDisplay.DisplayAsync(fullLibrary));
|
||||
}
|
||||
Shown += Form1_Shown;
|
||||
ApiExtended.LoginChoiceFactory = account => new WinformLoginChoiceEager(account, this);
|
||||
}
|
||||
|
||||
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
|
||||
|
||||
@ -351,7 +351,7 @@ namespace LibationWinForms.GridView
|
||||
.Select(lbe => lbe.LibraryBook)
|
||||
.Where(lb => !lb.Book.HasLiberated());
|
||||
|
||||
var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.CreateApiExtendedFunc(this), lib, accounts);
|
||||
var removedBooks = await LibraryCommands.FindInactiveBooks(lib, accounts);
|
||||
|
||||
var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user