diff --git a/Source/AaxDecrypter/AaxcDownloadConvertBase.cs b/Source/AaxDecrypter/AaxcDownloadConvertBase.cs index 42bf0ac8..f96dfeea 100644 --- a/Source/AaxDecrypter/AaxcDownloadConvertBase.cs +++ b/Source/AaxDecrypter/AaxcDownloadConvertBase.cs @@ -103,8 +103,8 @@ namespace AaxDecrypter OnInitialized(); OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged); - OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor ?? "[unknown]"); - OnRetrievedNarrators(AaxFile.AppleTags.Narrator ?? "[unknown]"); + OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor); + OnRetrievedNarrators(AaxFile.AppleTags.Narrator); OnRetrievedCoverArt(AaxFile.AppleTags.Cover); return !IsCanceled; diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs index 73e5fc2f..cea86b9b 100644 --- a/Source/AaxDecrypter/NetworkFileStream.cs +++ b/Source/AaxDecrypter/NetworkFileStream.cs @@ -61,9 +61,6 @@ namespace AaxDecrypter #region Constants - //Size of each range request. Android app uses 64MB chunks. - private const int RANGE_REQUEST_SZ = 64 * 1024 * 1024; - //Download memory buffer size private const int DOWNLOAD_BUFF_SZ = 8 * 1024; @@ -161,7 +158,7 @@ namespace AaxDecrypter //Initiate connection with the first request block and //get the total content length before returning. - using var client = new HttpClient(); + var client = new HttpClient(); var response = await RequestNextByteRangeAsync(client); if (ContentLength != 0 && ContentLength != response.FileSize) @@ -170,38 +167,59 @@ namespace AaxDecrypter ContentLength = response.FileSize; _downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset); - //Hand off the open request to the downloader to download and write data to file. - DownloadTask = Task.Run(() => DownloadLoopInternal(response), _cancellationSource.Token); + //Hand off the client and the open request to the downloader to download and write data to file. + DownloadTask = Task.Run(() => DownloadLoopInternal(client , response), _cancellationSource.Token); } - private async Task DownloadLoopInternal(BlockResponse initialResponse) + private async Task DownloadLoopInternal(HttpClient client, BlockResponse blockResponse) { - await DownloadToFile(initialResponse); - initialResponse.Dispose(); - try { - using var client = new HttpClient(); + long startPosition = WritePosition; + while (WritePosition < ContentLength && !IsCancelled) { - using var response = await RequestNextByteRangeAsync(client); - await DownloadToFile(response); + try + { + await DownloadToFile(blockResponse); + } + catch (HttpIOException e) + when (e.HttpRequestError is HttpRequestError.ResponseEnded + && WritePosition != startPosition + && WritePosition < ContentLength && !IsCancelled) + { + Serilog.Log.Logger.Debug($"The download connection ended before the file completed downloading all 0x{ContentLength:X10} bytes"); + + //the download made *some* progress since the last attempt. + //Try again to complete the download from where it left off. + //Make sure to rewind file to last flush position. + _writeFile.Position = startPosition = WritePosition; + blockResponse.Dispose(); + blockResponse = await RequestNextByteRangeAsync(client); + + Serilog.Log.Logger.Debug($"Resuming the file download starting at position 0x{WritePosition:X10}."); + } } } finally { - _writeFile.Close(); + _writeFile.Dispose(); + blockResponse.Dispose(); + client.Dispose(); } } private async Task RequestNextByteRangeAsync(HttpClient client) { - var request = new HttpRequestMessage(HttpMethod.Get, Uri); + using var request = new HttpRequestMessage(HttpMethod.Get, Uri); + + //Just in case it snuck in the saved json (Issue #1232) + RequestHeaders.Remove("Range"); foreach (var header in RequestHeaders) request.Headers.Add(header.Key, header.Value); - request.Headers.Add("Range", $"bytes={WritePosition}-{WritePosition + RANGE_REQUEST_SZ - 1}"); + request.Headers.Add("Range", $"bytes={WritePosition}-"); var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancellationSource.Token); @@ -226,7 +244,7 @@ namespace AaxDecrypter private async Task DownloadToFile(BlockResponse block) { var endPosition = WritePosition + block.BlockSize; - var networkStream = await block.Response.Content.ReadAsStreamAsync(_cancellationSource.Token); + using var networkStream = await block.Response.Content.ReadAsStreamAsync(_cancellationSource.Token); var downloadPosition = WritePosition; var nextFlush = downloadPosition + DATA_FLUSH_SZ; @@ -286,7 +304,6 @@ namespace AaxDecrypter } finally { - networkStream.Close(); _downloadedPiece.Set(); OnUpdate(); } diff --git a/Source/ApplicationServices/LibraryExporter.cs b/Source/ApplicationServices/LibraryExporter.cs index e2373e53..f0a62193 100644 --- a/Source/ApplicationServices/LibraryExporter.cs +++ b/Source/ApplicationServices/LibraryExporter.cs @@ -104,9 +104,6 @@ namespace ApplicationServices [Name("Content Type")] public string ContentType { get; set; } - [Name("Audio Format")] - public string AudioFormat { get; set; } - [Name("Language")] public string Language { get; set; } @@ -152,7 +149,6 @@ namespace ApplicationServices BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(), PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(), ContentType = a.Book.ContentType.ToString(), - AudioFormat = a.Book.AudioFormat.ToString(), Language = a.Book.Language, LastDownloaded = a.Book.UserDefinedItem.LastDownloaded, LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "", @@ -228,7 +224,6 @@ namespace ApplicationServices nameof(ExportDto.BookStatus), nameof(ExportDto.PdfStatus), nameof(ExportDto.ContentType), - nameof(ExportDto.AudioFormat), nameof(ExportDto.Language), nameof(ExportDto.LastDownloaded), nameof(ExportDto.LastDownloadedVersion), @@ -299,7 +294,6 @@ namespace ApplicationServices row.CreateCell(col++).SetCellValue(dto.BookStatus); row.CreateCell(col++).SetCellValue(dto.PdfStatus); row.CreateCell(col++).SetCellValue(dto.ContentType); - row.CreateCell(col++).SetCellValue(dto.AudioFormat); row.CreateCell(col++).SetCellValue(dto.Language); if (dto.LastDownloaded.HasValue) diff --git a/Source/DataLayer/Configurations/BookConfig.cs b/Source/DataLayer/Configurations/BookConfig.cs index f19e490d..bafa27e6 100644 --- a/Source/DataLayer/Configurations/BookConfig.cs +++ b/Source/DataLayer/Configurations/BookConfig.cs @@ -19,7 +19,6 @@ namespace DataLayer.Configurations // entity.Ignore(nameof(Book.Authors)); entity.Ignore(nameof(Book.Narrators)); - entity.Ignore(nameof(Book.AudioFormat)); entity.Ignore(nameof(Book.TitleWithSubtitle)); entity.Ignore(b => b.Categories); diff --git a/Source/DataLayer/EfClasses/AudioFormat.cs b/Source/DataLayer/EfClasses/AudioFormat.cs deleted file mode 100644 index b0f3374c..00000000 --- a/Source/DataLayer/EfClasses/AudioFormat.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; - -namespace DataLayer -{ - internal enum AudioFormatEnum : long - { - //Defining the enum this way ensures that when comparing: - //LC_128_44100_stereo > LC_64_44100_stereo > LC_64_22050_stereo > LC_64_22050_stereo - //This matches how audible interprets these codecs when specifying quality using AudibleApi.DownloadQuality - //I've never seen mono formats. - Unknown = 0, - LC_32_22050_stereo = (32L << 18) | (22050 << 2) | 2, - LC_64_22050_stereo = (64L << 18) | (22050 << 2) | 2, - LC_64_44100_stereo = (64L << 18) | (44100 << 2) | 2, - LC_128_44100_stereo = (128L << 18) | (44100 << 2) | 2, - AAX_22_32 = LC_32_22050_stereo, - AAX_22_64 = LC_64_22050_stereo, - AAX_44_64 = LC_64_44100_stereo, - AAX_44_128 = LC_128_44100_stereo - } - - public class AudioFormat : IComparable, IComparable - { - internal int AudioFormatID { get; private set; } - public int Bitrate { get; private init; } - public int SampleRate { get; private init; } - public int Channels { get; private init; } - public bool IsValid => Bitrate != 0 && SampleRate != 0 && Channels != 0; - - public static AudioFormat FromString(string formatStr) - { - if (Enum.TryParse(formatStr, ignoreCase: true, out AudioFormatEnum enumVal)) - return FromEnum(enumVal); - return FromEnum(AudioFormatEnum.Unknown); - } - - internal static AudioFormat FromEnum(AudioFormatEnum enumVal) - { - var val = (long)enumVal; - - return new() - { - Bitrate = (int)(val >> 18), - SampleRate = (int)(val >> 2) & ushort.MaxValue, - Channels = (int)(val & 3) - }; - } - internal AudioFormatEnum ToEnum() - { - var val = (AudioFormatEnum)(((long)Bitrate << 18) | ((long)SampleRate << 2) | (long)Channels); - - return Enum.IsDefined(val) ? - val : AudioFormatEnum.Unknown; - } - - public override string ToString() - => IsValid ? - $"{Bitrate} Kbps, {SampleRate / 1000d:F1} kHz, {(Channels == 2 ? "Stereo" : Channels)}" : - "Unknown"; - - public int CompareTo(AudioFormat other) => ToEnum().CompareTo(other.ToEnum()); - - public int CompareTo(object obj) => CompareTo(obj as AudioFormat); - } -} diff --git a/Source/DataLayer/EfClasses/Book.cs b/Source/DataLayer/EfClasses/Book.cs index 21c178fa..05ccd9e6 100644 --- a/Source/DataLayer/EfClasses/Book.cs +++ b/Source/DataLayer/EfClasses/Book.cs @@ -43,9 +43,9 @@ namespace DataLayer public ContentType ContentType { get; private set; } public string Locale { get; private set; } - internal AudioFormatEnum _audioFormat; - - public AudioFormat AudioFormat { get => AudioFormat.FromEnum(_audioFormat); set => _audioFormat = value.ToEnum(); } + //This field is now unused, however, there is little sense in adding a + //database migration to remove an unused field. Leave it for compatibility. + internal long _audioFormat; // mutable public string PictureId { get; set; } diff --git a/Source/DtoImporterService/BookImporter.cs b/Source/DtoImporterService/BookImporter.cs index 92ead237..95c86a64 100644 --- a/Source/DtoImporterService/BookImporter.cs +++ b/Source/DtoImporterService/BookImporter.cs @@ -154,9 +154,6 @@ namespace DtoImporterService // Update the book titles, since formatting can change book.UpdateTitle(item.Title, item.Subtitle); - var codec = item.AvailableCodecs?.Max(f => AudioFormat.FromString(f.EnhancedCodec)) ?? new AudioFormat(); - book.AudioFormat = codec; - // set/update book-specific info which may have changed if (item.PictureId is not null) book.PictureId = item.PictureId; diff --git a/Source/FileLiberator/AudioDecodable.cs b/Source/FileLiberator/AudioDecodable.cs index d29d8e85..d8213b53 100644 --- a/Source/FileLiberator/AudioDecodable.cs +++ b/Source/FileLiberator/AudioDecodable.cs @@ -19,21 +19,24 @@ namespace FileLiberator protected void OnTitleDiscovered(object _, string title) { Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(TitleDiscovered), Title = title }); - TitleDiscovered?.Invoke(this, title); + if (title != null) + TitleDiscovered?.Invoke(this, title); } protected void OnAuthorsDiscovered(string authors) => OnAuthorsDiscovered(null, authors); protected void OnAuthorsDiscovered(object _, string authors) { Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(AuthorsDiscovered), Authors = authors }); - AuthorsDiscovered?.Invoke(this, authors); + if (authors != null) + AuthorsDiscovered?.Invoke(this, authors); } protected void OnNarratorsDiscovered(string narrators) => OnNarratorsDiscovered(null, narrators); protected void OnNarratorsDiscovered(object _, string narrators) { Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(NarratorsDiscovered), Narrators = narrators }); - NarratorsDiscovered?.Invoke(this, narrators); + if (narrators != null) + NarratorsDiscovered?.Invoke(this, narrators); } protected byte[] OnRequestCoverArt() diff --git a/Source/FileLiberator/AudioFileStorageExt.cs b/Source/FileLiberator/AudioFileStorageExt.cs index ff47fd59..c0ce9bd0 100644 --- a/Source/FileLiberator/AudioFileStorageExt.cs +++ b/Source/FileLiberator/AudioFileStorageExt.cs @@ -39,14 +39,20 @@ namespace FileLiberator /// Path: in progress directory. /// File name: final file name. /// - public static string GetInProgressFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension) - => Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.DecryptInProgressDirectory, extension, returnFirstExisting: true); + public static string GetInProgressFilename(this AudioFileStorage _, LibraryBookDto libraryBook, string extension) + => Templates.File.GetFilename(libraryBook, AudibleFileStorage.DecryptInProgressDirectory, extension, returnFirstExisting: true); /// /// PDF: audio file does not exist /// public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension) => Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.BooksDirectory, extension); + + /// + /// PDF: audio file does not exist + /// + public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBookDto dto, string extension) + => Templates.File.GetFilename(dto, AudibleFileStorage.BooksDirectory, extension); /// /// PDF: audio file already exists diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index b6de8021..f6a70deb 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -47,13 +47,18 @@ namespace FileLiberator if (libraryBook.Book.Audio_Exists()) return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; + downloadValidation(libraryBook); + var api = await libraryBook.GetApiAsync(); + var config = Configuration.Instance; + using var downloadOptions = await DownloadOptions.InitiateDownloadAsync(api, config, libraryBook); + bool success = false; try { FilePathCache.Inserted += FilePathCache_Inserted; FilePathCache.Removed += FilePathCache_Removed; - success = await downloadAudiobookAsync(libraryBook); + success = await downloadAudiobookAsync(api, config, downloadOptions); } finally { @@ -78,12 +83,12 @@ namespace FileLiberator var finalStorageDir = getDestinationDirectory(libraryBook); var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries)); - Task[] finalTasks = new[] - { - Task.Run(() => downloadCoverArt(libraryBook)), + Task[] finalTasks = + [ + Task.Run(() => downloadCoverArt(downloadOptions)), moveFilesTask, Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir)) - }; + ]; try { @@ -116,16 +121,9 @@ namespace FileLiberator } } - private async Task downloadAudiobookAsync(LibraryBook libraryBook) + private async Task downloadAudiobookAsync(AudibleApi.Api api, Configuration config, DownloadOptions dlOptions) { - var config = Configuration.Instance; - - downloadValidation(libraryBook); - - var api = await libraryBook.GetApiAsync(); - - using var dlOptions = await DownloadOptions.InitiateDownloadAsync(api, libraryBook, config); - var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower()); + var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(dlOptions.LibraryBookDto, dlOptions.OutputFormat.ToString().ToLower()); var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory; if (dlOptions.DrmType is not DrmType.Adrm and not DrmType.Widevine) @@ -149,7 +147,7 @@ namespace FileLiberator abDownloader.RetrievedAuthors += OnAuthorsDiscovered; abDownloader.RetrievedNarrators += OnNarratorsDiscovered; abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt; - abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path); + abDownloader.FileCreated += (_, path) => OnFileCreated(dlOptions.LibraryBook, path); // REAL WORK DONE HERE var success = await abDownloader.RunAsync(); @@ -158,12 +156,12 @@ namespace FileLiberator { var metadataFile = LibationFileManager.Templates.Templates.File.GetFilename(dlOptions.LibraryBookDto, Path.GetDirectoryName(outFileName), ".metadata.json"); - var item = await api.GetCatalogProductAsync(libraryBook.Book.AudibleProductId, AudibleApi.CatalogOptions.ResponseGroupOptions.ALL_OPTIONS); + var item = await api.GetCatalogProductAsync(dlOptions.LibraryBook.Book.AudibleProductId, AudibleApi.CatalogOptions.ResponseGroupOptions.ALL_OPTIONS); item.SourceJson.Add(nameof(ContentMetadata.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(dlOptions.ContentMetadata.ChapterInfo)); item.SourceJson.Add(nameof(ContentMetadata.ContentReference), Newtonsoft.Json.Linq.JObject.FromObject(dlOptions.ContentMetadata.ContentReference)); File.WriteAllText(metadataFile, item.SourceJson.ToString()); - OnFileCreated(libraryBook, metadataFile); + OnFileCreated(dlOptions.LibraryBook, metadataFile); } return success; } @@ -173,7 +171,7 @@ namespace FileLiberator if (sender is not AaxcDownloadConvertBase converter || converter.DownloadOptions is not DownloadOptions options) return; - tags.Title ??= options.LibraryBookDto.TitleWithSubtitle; + tags.Title ??= options.LibraryBookDto.TitleWithSubtitle; tags.Album ??= tags.Title; tags.Artist ??= string.Join("; ", options.LibraryBook.Book.Authors.Select(a => a.Name)); tags.AlbumArtists ??= tags.Artist; @@ -280,7 +278,7 @@ namespace FileLiberator private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable entries) => entries.FirstOrDefault(f => f.FileType == FileType.Audio); - private static void downloadCoverArt(LibraryBook libraryBook) + private static void downloadCoverArt(DownloadOptions options) { if (!Configuration.Instance.DownloadCoverArt) return; @@ -288,24 +286,24 @@ namespace FileLiberator try { - var destinationDir = getDestinationDirectory(libraryBook); - coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, ".jpg"); + var destinationDir = getDestinationDirectory(options.LibraryBook); + coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(options.LibraryBookDto, ".jpg"); coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath)); if (File.Exists(coverPath)) FileUtility.SaferDelete(coverPath); - var picBytes = PictureStorage.GetPictureSynchronously(new(libraryBook.Book.PictureLarge ?? libraryBook.Book.PictureId, PictureSize.Native)); + var picBytes = PictureStorage.GetPictureSynchronously(new(options.LibraryBook.Book.PictureLarge ?? options.LibraryBook.Book.PictureId, PictureSize.Native)); if (picBytes.Length > 0) { File.WriteAllBytes(coverPath, picBytes); - SetFileTime(libraryBook, coverPath); + SetFileTime(options.LibraryBook, coverPath); } } catch (Exception ex) { //Failure to download cover art should not be considered a failure to download the book - Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {libraryBook.Book.AudibleProductId} to {coverPath} catalog product."); + Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {options.LibraryBook.Book.AudibleProductId} to {coverPath} catalog product."); } } } diff --git a/Source/FileLiberator/DownloadOptions.Factory.cs b/Source/FileLiberator/DownloadOptions.Factory.cs index 83cf93de..6e9d71ac 100644 --- a/Source/FileLiberator/DownloadOptions.Factory.cs +++ b/Source/FileLiberator/DownloadOptions.Factory.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; +using System.Text.RegularExpressions; using System.Threading.Tasks; #nullable enable @@ -23,7 +24,7 @@ public partial class DownloadOptions /// /// Initiate an audiobook download from the audible api. /// - public static async Task InitiateDownloadAsync(Api api, LibraryBook libraryBook, Configuration config) + public static async Task InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook) { var license = await ChooseContent(api, libraryBook, config); var options = BuildDownloadOptions(libraryBook, config, license); @@ -33,81 +34,78 @@ public partial class DownloadOptions private static async Task ChooseContent(Api api, LibraryBook libraryBook, Configuration config) { - var cdm = await Cdm.GetCdmAsync(); - var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High; - ContentLicense? contentLic = null; - ContentLicense? fallback = null; + if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm) + return await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality); - if (cdm is null) + ContentLicense? contentLic = null, fallback = null; + + try { - //Doesn't matter what the user chose. We can't get a CDM so we must fall back to AAX(C) - contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality); + //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)}") + }; + + contentLic + = await api.GetDownloadLicenseAsync( + libraryBook.Book.AudibleProductId, + dlQuality, + ChapterTitlesType.Tree, + DrmType.Widevine, + config.RequestSpatial, + codecChoice); } - else + catch (Exception ex) { - var spatial = config.FileDownloadQuality is Configuration.DownloadQuality.Spatial; - try - { - var codecChoice = config.SpatialAudioCodec switch - { - Configuration.SpatialCodec.EC_3 => Ec3Codec, - Configuration.SpatialCodec.AC_4 => Ac4Codec, - _ => throw new NotSupportedException($"Unknown value for {nameof(config.SpatialAudioCodec)}") - }; - contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality, ChapterTitlesType.Tree, DrmType.Widevine, spatial, codecChoice); - } - catch (Exception ex) - { - Serilog.Log.Logger.Error(ex, "Failed to request a Widevine license."); - } - - if (contentLic is null) - { - //We failed to get a widevine license, so fall back to AAX(C) - contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality); - } - else 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. - - 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; - } - } + 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.DrmType == DrmType.Widevine && cdm is not null) + 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. + + 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(); - var mpdResponse = await client.GetAsync(contentLic.LicenseResponse); + using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse); var dash = new MpegDash(mpdResponse.Content.ReadAsStream()); if (!dash.TryGetUri(new Uri(contentLic.LicenseResponse), out var contentUri)) @@ -124,7 +122,6 @@ public partial class DownloadOptions Key = Convert.ToHexStringLower(keys[0].Kid.ToByteArray()), Iv = Convert.ToHexStringLower(keys[0].Key) }; - } catch { @@ -160,9 +157,6 @@ public partial class DownloadOptions : contentLic.DrmType is DrmType.Adrm && contentLic.Voucher?.Key.Length == 32 && contentLic.Voucher?.Iv.Length == 32 ? AAXClean.FileType.Aaxc : null; - //Set the requested AudioFormat for use in file naming templates - libraryBook.Book.AudioFormat = AudioFormat.FromString(contentLic.ContentMetadata.ContentReference.ContentFormat); - var dlOptions = new DownloadOptions(config, libraryBook, contentLic.ContentMetadata.ContentUrl?.OfflineUrl) { AudibleKey = contentLic.Voucher?.Key, @@ -176,6 +170,14 @@ public partial class DownloadOptions RuntimeLength = TimeSpan.FromMilliseconds(contentLic.ContentMetadata.ChapterInfo.RuntimeLengthMs), }; + dlOptions.LibraryBookDto.Codec = contentLic.ContentMetadata.ContentReference.Codec; + if (TryGetAudioInfo(contentLic.ContentMetadata.ContentUrl, out int? bitrate, out int? sampleRate, out int? channels)) + { + dlOptions.LibraryBookDto.BitRate = bitrate; + dlOptions.LibraryBookDto.SampleRate = sampleRate; + dlOptions.LibraryBookDto.Channels = channels; + } + var titleConcat = config.CombineNestedChapterTitles ? ": " : null; var chapters = flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters, titleConcat) @@ -202,6 +204,43 @@ public partial class DownloadOptions return dlOptions; } + /// + /// The most reliable way to get these audio file properties is from the filename itself. + /// Using AAXClean to read the metadata works well for everything except AC-4 bitrate. + /// + private static bool TryGetAudioInfo(ContentUrl? contentUrl, out int? bitrate, out int? sampleRate, out int? channels) + { + bitrate = sampleRate = channels = null; + + if (contentUrl?.OfflineUrl is not string url || !Uri.TryCreate(url, default, out var uri)) + return false; + + var file = Path.GetFileName(uri.LocalPath); + + var match = AdrmAudioProperties().Match(file); + if (match.Success) + { + bitrate = int.Parse(match.Groups[1].Value); + sampleRate = int.Parse(match.Groups[2].Value); + channels = int.Parse(match.Groups[3].Value); + return true; + } + else if ((match = WidevineAudioProperties().Match(file)).Success) + { + bitrate = int.Parse(match.Groups[2].Value); + sampleRate = int.Parse(match.Groups[1].Value) * 1000; + channels = match.Groups[3].Value switch + { + "ec3" => 6, + "ac4" => 3, + _ => null + }; + return true; + } + + return false; + } + public static LameConfig GetLameOptions(Configuration config) { LameConfig lameConfig = new() @@ -359,4 +398,9 @@ public partial class DownloadOptions static double RelativePercentDifference(long num1, long num2) => Math.Abs(num1 - num2) / (double)(num1 + num2); + + [GeneratedRegex(@".+_(\d+)_(\d+)-(\w+).mp4", RegexOptions.Singleline | RegexOptions.IgnoreCase)] + private static partial Regex WidevineAudioProperties(); + [GeneratedRegex(@".+_lc_(\d+)_(\d+)_(\d+).aax", RegexOptions.Singleline | RegexOptions.IgnoreCase)] + private static partial Regex AdrmAudioProperties(); } diff --git a/Source/FileLiberator/UtilityExtensions.cs b/Source/FileLiberator/UtilityExtensions.cs index 6a920990..08f11a50 100644 --- a/Source/FileLiberator/UtilityExtensions.cs +++ b/Source/FileLiberator/UtilityExtensions.cs @@ -55,9 +55,6 @@ namespace FileLiberator IsPodcastParent = libraryBook.Book.IsEpisodeParent(), IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(), - BitRate = libraryBook.Book.AudioFormat.Bitrate, - SampleRate = libraryBook.Book.AudioFormat.SampleRate, - Channels = libraryBook.Book.AudioFormat.Channels, Language = libraryBook.Book.Language }; } diff --git a/Source/LibationAvalonia/Controls/Settings/Audio.axaml b/Source/LibationAvalonia/Controls/Settings/Audio.axaml index fc0b09eb..e99477ae 100644 --- a/Source/LibationAvalonia/Controls/Settings/Audio.axaml +++ b/Source/LibationAvalonia/Controls/Settings/Audio.axaml @@ -43,14 +43,40 @@ - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs b/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs index c2be0cfc..6f247581 100644 --- a/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs +++ b/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs @@ -22,21 +22,21 @@ namespace LibationAvalonia.Controls.Settings } } - - public async void Quality_SelectionChanged(object sender, SelectionChangedEventArgs e) + private async void UseWidevine_IsCheckedChanged(object sender, Avalonia.Interactivity.RoutedEventArgs e) { - if (_viewModel.SpatialSelected) + if (sender is CheckBox cbox && cbox.IsChecked is true) { using var accounts = AudibleApiStorage.GetAccountsSettingsPersister(); if (!accounts.AccountsSettings.Accounts.Any(a => a.IdentityTokens.DeviceType == AudibleApi.Resources.DeviceType)) { - await MessageBox.Show(VisualRoot as Window, - "Your must remove account(s) from Libation and then re-add them to enable spatial audiobook downloads.", - "Spatial Audio Unavailable", + 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.", + "Widevine Content Unavailable", MessageBoxButtons.OK); - _viewModel.FileDownloadQuality = _viewModel.DownloadQualities[1]; + _viewModel.UseWidevine = false; } } } diff --git a/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs index da6f5fbb..fe30623c 100644 --- a/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs @@ -114,7 +114,6 @@ Title: {title} Author(s): {Book.AuthorNames()} Narrator(s): {Book.NarratorNames()} Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")} -Audio Bitrate: {Book.AudioFormat} Category: {string.Join(", ", Book.LowestCategoryNames())} Purchase Date: {libraryBook.DateAdded:d} Language: {Book.Language} diff --git a/Source/LibationAvalonia/ViewModels/Settings/AudioSettingsVM.cs b/Source/LibationAvalonia/ViewModels/Settings/AudioSettingsVM.cs index 7f923cc0..17fb37e9 100644 --- a/Source/LibationAvalonia/ViewModels/Settings/AudioSettingsVM.cs +++ b/Source/LibationAvalonia/ViewModels/Settings/AudioSettingsVM.cs @@ -67,6 +67,8 @@ namespace LibationAvalonia.ViewModels.Settings FileDownloadQuality = DownloadQualities.SingleOrDefault(s => s.Value == config.FileDownloadQuality) ?? DownloadQualities[0]; SelectedSampleRate = SampleRates.SingleOrDefault(s => s.Value == config.MaxSampleRate) ?? SampleRates[0]; SelectedEncoderQuality = config.LameEncoderQuality; + UseWidevine = config.UseWidevine; + RequestSpatial = config.RequestSpatial; } public void SaveSettings(Configuration config) @@ -96,12 +98,13 @@ namespace LibationAvalonia.ViewModels.Settings config.MaxSampleRate = SelectedSampleRate?.Value ?? config.MaxSampleRate; config.FileDownloadQuality = FileDownloadQuality?.Value ?? config.FileDownloadQuality; config.SpatialAudioCodec = SpatialAudioCodec?.Value ?? config.SpatialAudioCodec; + config.UseWidevine = UseWidevine; + config.RequestSpatial = RequestSpatial; } public AvaloniaList> DownloadQualities { get; } = new([ new EnumDisplay(Configuration.DownloadQuality.Normal), new EnumDisplay(Configuration.DownloadQuality.High), - new EnumDisplay(Configuration.DownloadQuality.Spatial, "Spatial (if available)"), ]); public AvaloniaList> SpatialAudioCodecs { get; } = new([ new EnumDisplay(Configuration.SpatialCodec.EC_3, "Dolby Digital Plus (E-AC-3)"), @@ -109,6 +112,10 @@ namespace LibationAvalonia.ViewModels.Settings ]); public AvaloniaList ClipBookmarkFormats { get; } = new(Enum.GetValues()); public string FileDownloadQualityText { get; } = Configuration.GetDescription(nameof(Configuration.FileDownloadQuality)); + public string UseWidevineText { get; } = Configuration.GetDescription(nameof(Configuration.UseWidevine)); + public string UseWidevineTip { get; } = Configuration.GetHelpText(nameof(Configuration.UseWidevine)); + public string RequestSpatialText { get; } = Configuration.GetDescription(nameof(Configuration.RequestSpatial)); + public string RequestSpatialTip { get; } = Configuration.GetHelpText(nameof(Configuration.RequestSpatial)); public string SpatialAudioCodecText { get; } = Configuration.GetDescription(nameof(Configuration.SpatialAudioCodec)); public string SpatialAudioCodecTip { get; } = Configuration.GetHelpText(nameof(Configuration.SpatialAudioCodec)); public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet)); @@ -133,19 +140,13 @@ namespace LibationAvalonia.ViewModels.Settings public string RetainAaxFileTip => Configuration.GetHelpText(nameof(RetainAaxFile)); public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); } - public bool SpatialSelected { get; private set; } - private EnumDisplay? _fileDownloadQuality; - public EnumDisplay FileDownloadQuality - { - get => _fileDownloadQuality ?? DownloadQualities[0]; - set - { - SpatialSelected = value?.Value == Configuration.DownloadQuality.Spatial; - this.RaiseAndSetIfChanged(ref _fileDownloadQuality, value); - this.RaisePropertyChanged(nameof(SpatialSelected)); - } - } + private bool _useWidevine; + private bool _requestSpatial; + public bool UseWidevine { get => _useWidevine; set => this.RaiseAndSetIfChanged(ref _useWidevine, value); } + public bool RequestSpatial { get => _requestSpatial; set => this.RaiseAndSetIfChanged(ref _requestSpatial, value); } + + public EnumDisplay FileDownloadQuality { get; set; } public EnumDisplay SpatialAudioCodec { get; set; } public Configuration.ClipBookmarkFormat ClipBookmarkFormat { get; set; } public bool MergeOpeningAndEndCredits { get; set; } diff --git a/Source/LibationFileManager/Configuration.HelpText.cs b/Source/LibationFileManager/Configuration.HelpText.cs index 8fdefaa8..26371122 100644 --- a/Source/LibationFileManager/Configuration.HelpText.cs +++ b/Source/LibationFileManager/Configuration.HelpText.cs @@ -89,6 +89,24 @@ namespace LibationFileManager AC-4 cannot be converted to MP3. """ }, + {nameof(UseWidevine), """ + Some audiobooks are only delivered in the highest + available quality with special, third-party content + protection. Enabling this option will make Libation + request audiobooks with Widevine DRM, which may + yield higher quality audiobook files. If they are + higher quality, however, they will also be encoded + with a somewhat uncommon codec (xHE-AAC USAC) + which you may have difficulty playing. + + This must be enable to download spatial audiobooks. + """ }, + {nameof(RequestSpatial), """ + If selected, Libation will request audiobooks in the + Dolby Atmos 'Spatial Audio' format. Audiobooks which + don't have a spatial audio version will be download + as usual based on your other file quality settings. + """ }, } .AsReadOnly(); diff --git a/Source/LibationFileManager/Configuration.PersistentSettings.cs b/Source/LibationFileManager/Configuration.PersistentSettings.cs index c2c58c50..7da1c25c 100644 --- a/Source/LibationFileManager/Configuration.PersistentSettings.cs +++ b/Source/LibationFileManager/Configuration.PersistentSettings.cs @@ -246,8 +246,7 @@ namespace LibationFileManager public enum DownloadQuality { High, - Normal, - Spatial + Normal } [JsonConverter(typeof(StringEnumConverter))] @@ -257,6 +256,12 @@ namespace LibationFileManager AC_4 } + [Description("Use widevine DRM")] + public bool UseWidevine { get => GetNonString(defaultValue: true); set => SetNonString(value); } + + [Description("Request Spatial Audio")] + public bool RequestSpatial { get => GetNonString(defaultValue: true); set => SetNonString(value); } + [Description("Spatial audio codec:")] public SpatialCodec SpatialAudioCodec { get => GetNonString(defaultValue: SpatialCodec.EC_3); set => SetNonString(value); } diff --git a/Source/LibationFileManager/Templates/LibraryBookDto.cs b/Source/LibationFileManager/Templates/LibraryBookDto.cs index be26c989..dc32fa9c 100644 --- a/Source/LibationFileManager/Templates/LibraryBookDto.cs +++ b/Source/LibationFileManager/Templates/LibraryBookDto.cs @@ -27,9 +27,10 @@ public class BookDto public bool IsPodcastParent { get; set; } public bool IsPodcast { get; set; } - public int BitRate { get; set; } - public int SampleRate { get; set; } - public int Channels { get; set; } + public int? BitRate { get; set; } + public int? SampleRate { get; set; } + public int? Channels { get; set; } + public string? Codec { get; set; } public DateTime FileDate { get; set; } = DateTime.Now; public DateTime? DatePublished { get; set; } public string? Language { get; set; } diff --git a/Source/LibationFileManager/Templates/TemplateTags.cs b/Source/LibationFileManager/Templates/TemplateTags.cs index d5bead6d..e8cdb1df 100644 --- a/Source/LibationFileManager/Templates/TemplateTags.cs +++ b/Source/LibationFileManager/Templates/TemplateTags.cs @@ -36,9 +36,10 @@ namespace LibationFileManager.Templates public static TemplateTags Series { get; } = new TemplateTags("series", "All series to which the book belongs (if any)"); public static TemplateTags FirstSeries { get; } = new TemplateTags("first series", "First series"); public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series (alias for "); - public static TemplateTags Bitrate { get; } = new TemplateTags("bitrate", "File's orig. bitrate"); - public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "File's orig. sample rate"); - public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels"); + public static TemplateTags Bitrate { get; } = new TemplateTags("bitrate", "Audiobook's source bitrate"); + public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "Audiobook's source sample rate"); + public static TemplateTags Channels { get; } = new TemplateTags("channels", "Audiobook's source audio channel count"); + public static TemplateTags Codec { get; } = new TemplateTags("codec", "Audiobook's source codec"); public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book"); public static TemplateTags AccountNickname { get; } = new TemplateTags("account nickname", "Audible account nickname of this book"); public static TemplateTags Locale { get; } = new("locale", "Region/country"); diff --git a/Source/LibationFileManager/Templates/Templates.cs b/Source/LibationFileManager/Templates/Templates.cs index c054459d..bfba1468 100644 --- a/Source/LibationFileManager/Templates/Templates.cs +++ b/Source/LibationFileManager/Templates/Templates.cs @@ -271,9 +271,6 @@ namespace LibationFileManager.Templates { TemplateTags.Language, lb => lb.Language }, //Don't allow formatting of LanguageShort { TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort }, - { TemplateTags.Bitrate, lb => (int?)(lb.IsPodcastParent ? null : lb.BitRate) }, - { TemplateTags.SampleRate, lb => (int?)(lb.IsPodcastParent ? null : lb.SampleRate) }, - { TemplateTags.Channels, lb => (int?)(lb.IsPodcastParent ? null : lb.Channels) }, { TemplateTags.Account, lb => lb.Account }, { TemplateTags.AccountNickname, lb => lb.AccountNickname }, { TemplateTags.Locale, lb => lb.Locale }, @@ -281,7 +278,16 @@ namespace LibationFileManager.Templates { TemplateTags.DatePublished, lb => lb.DatePublished }, { TemplateTags.DateAdded, lb => lb.DateAdded }, { TemplateTags.FileDate, lb => lb.FileDate }, - }; + }; + + private static readonly PropertyTagCollection audioFilePropertyTags = + new(caseSensative: true, StringFormatter, IntegerFormatter) + { + { TemplateTags.Bitrate, lb => lb.BitRate }, + { TemplateTags.SampleRate, lb => lb.SampleRate }, + { TemplateTags.Channels, lb => lb.Channels }, + { TemplateTags.Codec, lb => lb.Codec }, + }; private static readonly List chapterPropertyTags = new() { @@ -376,8 +382,7 @@ namespace LibationFileManager.Templates public static string Name { get; } = "Folder Template"; public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? ""; public static string DefaultTemplate { get; } = " [<id>]"; - public static IEnumerable<TagCollection> TagCollections - => new TagCollection[] { filePropertyTags, conditionalTags, folderConditionalTags }; + public static IEnumerable<TagCollection> TagCollections { get; } = [filePropertyTags, conditionalTags, folderConditionalTags]; public override IEnumerable<string> Errors => TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ERROR_FULL_PATH_IS_INVALID) : base.Errors; @@ -396,7 +401,7 @@ namespace LibationFileManager.Templates public static string Name { get; } = "File Template"; public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate)) ?? ""; public static string DefaultTemplate { get; } = "<title> [<id>]"; - public static IEnumerable<TagCollection> TagCollections { get; } = new TagCollection[] { filePropertyTags, conditionalTags }; + public static IEnumerable<TagCollection> TagCollections { get; } = [filePropertyTags, audioFilePropertyTags, conditionalTags]; } public class ChapterFileTemplate : Templates, ITemplate @@ -404,7 +409,8 @@ namespace LibationFileManager.Templates public static string Name { get; } = "Chapter File Template"; public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)) ?? ""; public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>"; - public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags); + public static IEnumerable<TagCollection> TagCollections { get; } + = chapterPropertyTags.Append(filePropertyTags).Append(audioFilePropertyTags).Append(conditionalTags); public override IEnumerable<string> Warnings => NamingTemplate.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName)) diff --git a/Source/LibationWinForms/Dialogs/BookDetailsDialog.cs b/Source/LibationWinForms/Dialogs/BookDetailsDialog.cs index c534e40b..93d91e8d 100644 --- a/Source/LibationWinForms/Dialogs/BookDetailsDialog.cs +++ b/Source/LibationWinForms/Dialogs/BookDetailsDialog.cs @@ -49,7 +49,6 @@ Title: {title} Author(s): {Book.AuthorNames()} Narrator(s): {Book.NarratorNames()} Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")} -Audio Bitrate: {Book.AudioFormat} Category: {string.Join(", ", Book.LowestCategoryNames())} Purchase Date: {_libraryBook.DateAdded:d} Language: {Book.Language} diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs index 1127125a..c894e5ca 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs @@ -23,6 +23,8 @@ namespace LibationWinForms.Dialogs this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio)); this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged)); this.moveMoovAtomCbox.Text = desc(nameof(config.MoveMoovToBeginning)); + this.useWidevineCbox.Text = desc(nameof(config.UseWidevine)); + this.requestSpatialCbox.Text = desc(nameof(config.RequestSpatial)); this.spatialCodecLbl.Text = desc(nameof(config.SpatialAudioCodec)); toolTip.SetToolTip(combineNestedChapterTitlesCbox, Configuration.GetHelpText(nameof(config.CombineNestedChapterTitles))); @@ -34,6 +36,8 @@ namespace LibationWinForms.Dialogs toolTip.SetToolTip(mergeOpeningEndCreditsCbox, Configuration.GetHelpText(nameof(config.MergeOpeningAndEndCredits))); toolTip.SetToolTip(retainAaxFileCbox, Configuration.GetHelpText(nameof(config.RetainAaxFile))); toolTip.SetToolTip(stripAudibleBrandingCbox, Configuration.GetHelpText(nameof(config.StripAudibleBrandAudio))); + toolTip.SetToolTip(useWidevineCbox, Configuration.GetHelpText(nameof(config.UseWidevine))); + toolTip.SetToolTip(requestSpatialCbox, Configuration.GetHelpText(nameof(config.RequestSpatial))); toolTip.SetToolTip(spatialCodecLbl, Configuration.GetHelpText(nameof(config.SpatialAudioCodec))); toolTip.SetToolTip(spatialAudioCodecCb, Configuration.GetHelpText(nameof(config.SpatialAudioCodec))); @@ -41,7 +45,6 @@ namespace LibationWinForms.Dialogs [ new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.Normal), new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.High), - new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.Spatial, "Spatial (if available)"), ]); spatialAudioCodecCb.Items.AddRange( @@ -76,6 +79,8 @@ namespace LibationWinForms.Dialogs downloadClipsBookmarksCbox.Checked = config.DownloadClipsBookmarks; fileDownloadQualityCb.SelectedItem = config.FileDownloadQuality; spatialAudioCodecCb.SelectedItem = config.SpatialAudioCodec; + useWidevineCbox.Checked = config.UseWidevine; + requestSpatialCbox.Checked = config.RequestSpatial; clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat; retainAaxFileCbox.Checked = config.RetainAaxFile; @@ -118,6 +123,8 @@ namespace LibationWinForms.Dialogs config.DownloadCoverArt = downloadCoverArtCbox.Checked; config.DownloadClipsBookmarks = downloadClipsBookmarksCbox.Checked; config.FileDownloadQuality = ((EnumDisplay<Configuration.DownloadQuality>)fileDownloadQualityCb.SelectedItem).Value; + config.UseWidevine = useWidevineCbox.Checked; + config.RequestSpatial = requestSpatialCbox.Checked; config.SpatialAudioCodec = ((EnumDisplay<Configuration.SpatialCodec>)spatialAudioCodecCb.SelectedItem).Value; config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem; config.RetainAaxFile = retainAaxFileCbox.Checked; @@ -140,7 +147,6 @@ namespace LibationWinForms.Dialogs config.ChapterTitleTemplate = chapterTitleTemplateTb.Text; } - private void downloadClipsBookmarksCbox_CheckedChanged(object sender, EventArgs e) { clipsBookmarksFormatCb.Enabled = downloadClipsBookmarksCbox.Checked; @@ -190,27 +196,28 @@ namespace LibationWinForms.Dialogs } } - private void fileDownloadQualityCb_SelectedIndexChanged(object sender, EventArgs e) - { - var selectedSpatial = fileDownloadQualityCb.SelectedItem.Equals(Configuration.DownloadQuality.Spatial); - if (selectedSpatial) + + private void useWidevineCbox_CheckedChanged(object sender, EventArgs e) + { + if (useWidevineCbox.Checked) { using var accounts = AudibleApiStorage.GetAccountsSettingsPersister(); 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 spatial audiobook downloads.", - "Spatial Audio Unavailable", + "Your must remove account(s) from Libation and then re-add them to enable widwvine content.", + "Widevine Content Unavailable", MessageBoxButtons.OK); - fileDownloadQualityCb.SelectedItem = Configuration.DownloadQuality.High; + useWidevineCbox.Checked = false; return; } } - - spatialCodecLbl.Enabled = spatialAudioCodecCb.Enabled = selectedSpatial; + requestSpatialCbox.Enabled = useWidevineCbox.Checked; + spatialCodecLbl.Enabled = spatialAudioCodecCb.Enabled = useWidevineCbox.Checked && requestSpatialCbox.Checked; } + } } diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs index 232e37d3..1f8ef5ab 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs @@ -84,6 +84,8 @@ folderTemplateTb = new System.Windows.Forms.TextBox(); folderTemplateLbl = new System.Windows.Forms.Label(); tab4AudioFileOptions = new System.Windows.Forms.TabPage(); + requestSpatialCbox = new System.Windows.Forms.CheckBox(); + useWidevineCbox = new System.Windows.Forms.CheckBox(); spatialAudioCodecCb = new System.Windows.Forms.ComboBox(); spatialCodecLbl = new System.Windows.Forms.Label(); moveMoovAtomCbox = new System.Windows.Forms.CheckBox(); @@ -306,7 +308,7 @@ allowLibationFixupCbox.AutoSize = true; allowLibationFixupCbox.Checked = true; allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked; - allowLibationFixupCbox.Location = new System.Drawing.Point(19, 205); + allowLibationFixupCbox.Location = new System.Drawing.Point(19, 230); allowLibationFixupCbox.Name = "allowLibationFixupCbox"; allowLibationFixupCbox.Size = new System.Drawing.Size(162, 19); allowLibationFixupCbox.TabIndex = 11; @@ -772,6 +774,8 @@ // tab4AudioFileOptions // tab4AudioFileOptions.AutoScroll = true; + tab4AudioFileOptions.Controls.Add(requestSpatialCbox); + tab4AudioFileOptions.Controls.Add(useWidevineCbox); tab4AudioFileOptions.Controls.Add(spatialAudioCodecCb); tab4AudioFileOptions.Controls.Add(spatialCodecLbl); tab4AudioFileOptions.Controls.Add(moveMoovAtomCbox); @@ -798,11 +802,38 @@ tab4AudioFileOptions.Text = "Audio File Options"; tab4AudioFileOptions.UseVisualStyleBackColor = true; // + // requestSpatialCbox + // + requestSpatialCbox.AutoSize = true; + requestSpatialCbox.CheckAlign = System.Drawing.ContentAlignment.MiddleRight; + requestSpatialCbox.Checked = true; + requestSpatialCbox.CheckState = System.Windows.Forms.CheckState.Checked; + requestSpatialCbox.Location = new System.Drawing.Point(284, 35); + requestSpatialCbox.Name = "requestSpatialCbox"; + requestSpatialCbox.Size = new System.Drawing.Size(138, 19); + requestSpatialCbox.TabIndex = 29; + requestSpatialCbox.Text = "[RequestSpatial desc]"; + requestSpatialCbox.UseVisualStyleBackColor = true; + requestSpatialCbox.CheckedChanged += useWidevineCbox_CheckedChanged; + // + // useWidevineCbox + // + useWidevineCbox.AutoSize = true; + useWidevineCbox.Checked = true; + useWidevineCbox.CheckState = System.Windows.Forms.CheckState.Checked; + useWidevineCbox.Location = new System.Drawing.Point(19, 35); + useWidevineCbox.Name = "useWidevineCbox"; + useWidevineCbox.Size = new System.Drawing.Size(129, 19); + useWidevineCbox.TabIndex = 28; + useWidevineCbox.Text = "[UseWidevine desc]"; + useWidevineCbox.UseVisualStyleBackColor = true; + useWidevineCbox.CheckedChanged += useWidevineCbox_CheckedChanged; + // // spatialAudioCodecCb // spatialAudioCodecCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; spatialAudioCodecCb.FormattingEnabled = true; - spatialAudioCodecCb.Location = new System.Drawing.Point(249, 35); + spatialAudioCodecCb.Location = new System.Drawing.Point(249, 60); spatialAudioCodecCb.Margin = new System.Windows.Forms.Padding(3, 3, 5, 3); spatialAudioCodecCb.Name = "spatialAudioCodecCb"; spatialAudioCodecCb.Size = new System.Drawing.Size(173, 23); @@ -811,7 +842,7 @@ // spatialCodecLbl // spatialCodecLbl.AutoSize = true; - spatialCodecLbl.Location = new System.Drawing.Point(19, 37); + spatialCodecLbl.Location = new System.Drawing.Point(19, 62); spatialCodecLbl.Name = "spatialCodecLbl"; spatialCodecLbl.Size = new System.Drawing.Size(143, 15); spatialCodecLbl.TabIndex = 24; @@ -836,7 +867,6 @@ fileDownloadQualityCb.Name = "fileDownloadQualityCb"; fileDownloadQualityCb.Size = new System.Drawing.Size(130, 23); fileDownloadQualityCb.TabIndex = 1; - fileDownloadQualityCb.SelectedIndexChanged += fileDownloadQualityCb_SelectedIndexChanged; // // fileDownloadQualityLbl // @@ -851,7 +881,7 @@ // combineNestedChapterTitlesCbox // combineNestedChapterTitlesCbox.AutoSize = true; - combineNestedChapterTitlesCbox.Location = new System.Drawing.Point(19, 181); + combineNestedChapterTitlesCbox.Location = new System.Drawing.Point(19, 206); combineNestedChapterTitlesCbox.Name = "combineNestedChapterTitlesCbox"; combineNestedChapterTitlesCbox.Size = new System.Drawing.Size(217, 19); combineNestedChapterTitlesCbox.TabIndex = 10; @@ -862,7 +892,7 @@ // clipsBookmarksFormatCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; clipsBookmarksFormatCb.FormattingEnabled = true; - clipsBookmarksFormatCb.Location = new System.Drawing.Point(285, 107); + clipsBookmarksFormatCb.Location = new System.Drawing.Point(285, 132); clipsBookmarksFormatCb.Name = "clipsBookmarksFormatCb"; clipsBookmarksFormatCb.Size = new System.Drawing.Size(67, 23); clipsBookmarksFormatCb.TabIndex = 6; @@ -870,7 +900,7 @@ // downloadClipsBookmarksCbox // downloadClipsBookmarksCbox.AutoSize = true; - downloadClipsBookmarksCbox.Location = new System.Drawing.Point(19, 109); + downloadClipsBookmarksCbox.Location = new System.Drawing.Point(19, 134); downloadClipsBookmarksCbox.Name = "downloadClipsBookmarksCbox"; downloadClipsBookmarksCbox.Size = new System.Drawing.Size(248, 19); downloadClipsBookmarksCbox.TabIndex = 5; @@ -883,7 +913,7 @@ audiobookFixupsGb.Controls.Add(splitFilesByChapterCbox); audiobookFixupsGb.Controls.Add(stripUnabridgedCbox); audiobookFixupsGb.Controls.Add(stripAudibleBrandingCbox); - audiobookFixupsGb.Location = new System.Drawing.Point(6, 229); + audiobookFixupsGb.Location = new System.Drawing.Point(6, 254); audiobookFixupsGb.Name = "audiobookFixupsGb"; audiobookFixupsGb.Size = new System.Drawing.Size(416, 114); audiobookFixupsGb.TabIndex = 19; @@ -1324,7 +1354,7 @@ // mergeOpeningEndCreditsCbox // mergeOpeningEndCreditsCbox.AutoSize = true; - mergeOpeningEndCreditsCbox.Location = new System.Drawing.Point(19, 157); + mergeOpeningEndCreditsCbox.Location = new System.Drawing.Point(19, 182); mergeOpeningEndCreditsCbox.Name = "mergeOpeningEndCreditsCbox"; mergeOpeningEndCreditsCbox.Size = new System.Drawing.Size(198, 19); mergeOpeningEndCreditsCbox.TabIndex = 9; @@ -1334,7 +1364,7 @@ // retainAaxFileCbox // retainAaxFileCbox.AutoSize = true; - retainAaxFileCbox.Location = new System.Drawing.Point(19, 133); + retainAaxFileCbox.Location = new System.Drawing.Point(19, 158); retainAaxFileCbox.Name = "retainAaxFileCbox"; retainAaxFileCbox.Size = new System.Drawing.Size(131, 19); retainAaxFileCbox.TabIndex = 8; @@ -1347,7 +1377,7 @@ downloadCoverArtCbox.AutoSize = true; downloadCoverArtCbox.Checked = true; downloadCoverArtCbox.CheckState = System.Windows.Forms.CheckState.Checked; - downloadCoverArtCbox.Location = new System.Drawing.Point(19, 85); + downloadCoverArtCbox.Location = new System.Drawing.Point(19, 110); downloadCoverArtCbox.Name = "downloadCoverArtCbox"; downloadCoverArtCbox.Size = new System.Drawing.Size(162, 19); downloadCoverArtCbox.TabIndex = 4; @@ -1360,7 +1390,7 @@ createCueSheetCbox.AutoSize = true; createCueSheetCbox.Checked = true; createCueSheetCbox.CheckState = System.Windows.Forms.CheckState.Checked; - createCueSheetCbox.Location = new System.Drawing.Point(19, 61); + createCueSheetCbox.Location = new System.Drawing.Point(19, 86); createCueSheetCbox.Name = "createCueSheetCbox"; createCueSheetCbox.Size = new System.Drawing.Size(145, 19); createCueSheetCbox.TabIndex = 3; @@ -1531,5 +1561,7 @@ private System.Windows.Forms.Button applyDisplaySettingsBtn; private System.Windows.Forms.ComboBox spatialAudioCodecCb; private System.Windows.Forms.Label spatialCodecLbl; + private System.Windows.Forms.CheckBox useWidevineCbox; + private System.Windows.Forms.CheckBox requestSpatialCbox; } } \ No newline at end of file