From 1090d29f7471c72f431dd227ca4773233dd47772 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Thu, 1 May 2025 13:03:03 -0600 Subject: [PATCH 1/8] Add fine-grained options for downloading widevine content --- .../FileLiberator/DownloadOptions.Factory.cs | 124 +++++++++--------- .../Controls/Settings/Audio.axaml | 37 +++++- .../Controls/Settings/Audio.axaml.cs | 14 +- .../ViewModels/Settings/AudioSettingsVM.cs | 27 ++-- .../Configuration.HelpText.cs | 18 +++ .../Configuration.PersistentSettings.cs | 9 +- .../Dialogs/SettingsDialog.AudioSettings.cs | 29 ++-- .../Dialogs/SettingsDialog.Designer.cs | 56 ++++++-- 8 files changed, 202 insertions(+), 112 deletions(-) diff --git a/Source/FileLiberator/DownloadOptions.Factory.cs b/Source/FileLiberator/DownloadOptions.Factory.cs index 83cf93de..cfccd55c 100644 --- a/Source/FileLiberator/DownloadOptions.Factory.cs +++ b/Source/FileLiberator/DownloadOptions.Factory.cs @@ -33,81 +33,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 +121,6 @@ public partial class DownloadOptions Key = Convert.ToHexStringLower(keys[0].Kid.ToByteArray()), Iv = Convert.ToHexStringLower(keys[0].Key) }; - } catch { 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/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/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.Normal), new EnumDisplay(Configuration.DownloadQuality.High), - new EnumDisplay(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)fileDownloadQualityCb.SelectedItem).Value; + config.UseWidevine = useWidevineCbox.Checked; + config.RequestSpatial = requestSpatialCbox.Checked; config.SpatialAudioCodec = ((EnumDisplay)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 From f4dafac28f3f21f7749acc8f431da259bb809154 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Thu, 1 May 2025 13:19:03 -0600 Subject: [PATCH 2/8] Try to solve #1226 --- Source/AaxDecrypter/NetworkFileStream.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs index 73e5fc2f..95b76442 100644 --- a/Source/AaxDecrypter/NetworkFileStream.cs +++ b/Source/AaxDecrypter/NetworkFileStream.cs @@ -359,7 +359,7 @@ namespace AaxDecrypter /// The minimum required flushed data length in . private void WaitToPosition(long requiredPosition) { - while (WritePosition < requiredPosition + while (_readFile.Position < requiredPosition && DownloadTask?.IsCompleted is false && !IsCancelled) { From 3982edd0f121ad4c23f430750777648c3887ea91 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Fri, 2 May 2025 11:20:58 -0600 Subject: [PATCH 3/8] Add codec tag and use real bitrate/samplerate (#1227) --- Source/FileLiberator/AudioFileStorageExt.cs | 10 +++- Source/FileLiberator/DownloadDecryptBook.cs | 48 ++++++++--------- .../FileLiberator/DownloadOptions.Factory.cs | 53 ++++++++++++++++++- Source/FileLiberator/UtilityExtensions.cs | 3 -- .../Templates/LibraryBookDto.cs | 7 +-- .../Templates/TemplateTags.cs | 7 +-- .../Templates/Templates.cs | 22 +++++--- 7 files changed, 106 insertions(+), 44 deletions(-) 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..0f92ce8c 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,11 @@ 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 +149,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 +158,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 +173,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 +280,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 +288,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 cfccd55c..63db1f4f 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); @@ -172,6 +173,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) @@ -198,6 +207,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() @@ -355,4 +401,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/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)) From 3aebc7c885d8e4467c639acebc0a9759fe574a61 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Fri, 2 May 2025 11:42:51 -0600 Subject: [PATCH 4/8] Improve download performance. --- Source/AaxDecrypter/NetworkFileStream.cs | 50 +++++++++++++++--------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs index 95b76442..f482bd4f 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,54 @@ 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 < ContentLength && !IsCancelled) + { + //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); + } } } finally { - _writeFile.Close(); + _writeFile.Dispose(); + blockResponse.Dispose(); + client.Dispose(); } } private async Task<BlockResponse> 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 +239,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 +299,6 @@ namespace AaxDecrypter } finally { - networkStream.Close(); _downloadedPiece.Set(); OnUpdate(); } @@ -359,7 +371,7 @@ namespace AaxDecrypter /// <param name="requiredPosition">The minimum required flushed data length in <see cref="SaveFilePath"/>.</param> private void WaitToPosition(long requiredPosition) { - while (_readFile.Position < requiredPosition + while (WritePosition < requiredPosition && DownloadTask?.IsCompleted is false && !IsCancelled) { From 5f4551822b5e7674a6cfd9914c808f7a3d76e53a Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Fri, 2 May 2025 12:39:12 -0600 Subject: [PATCH 5/8] Remove Book.AudioFormat property This property was set to the highest quality returned by the library scan. Since adding quality option settings, it is no longer guaranteed to reflect the file that is downloaded. Also, the library scan qualities don't contain spatial audio or widevine-specific qualities., only ADRM. --- Source/ApplicationServices/LibraryExporter.cs | 1 - Source/DataLayer/Configurations/BookConfig.cs | 1 - Source/DataLayer/EfClasses/AudioFormat.cs | 65 ------------------- Source/DataLayer/EfClasses/Book.cs | 6 +- Source/DtoImporterService/BookImporter.cs | 3 - .../FileLiberator/DownloadOptions.Factory.cs | 3 - .../Dialogs/BookDetailsDialog.axaml.cs | 1 - .../Dialogs/BookDetailsDialog.cs | 1 - 8 files changed, 3 insertions(+), 78 deletions(-) delete mode 100644 Source/DataLayer/EfClasses/AudioFormat.cs diff --git a/Source/ApplicationServices/LibraryExporter.cs b/Source/ApplicationServices/LibraryExporter.cs index e2373e53..b299c1b2 100644 --- a/Source/ApplicationServices/LibraryExporter.cs +++ b/Source/ApplicationServices/LibraryExporter.cs @@ -152,7 +152,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() ?? "", 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<AudioFormat>, 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/DownloadOptions.Factory.cs b/Source/FileLiberator/DownloadOptions.Factory.cs index 63db1f4f..6e9d71ac 100644 --- a/Source/FileLiberator/DownloadOptions.Factory.cs +++ b/Source/FileLiberator/DownloadOptions.Factory.cs @@ -157,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, 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/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} From ce952417fbc85856032c2f5598c24b393b76acd4 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Fri, 2 May 2025 13:07:53 -0600 Subject: [PATCH 6/8] Don't replace library properties in queued item with null/empty --- Source/AaxDecrypter/AaxcDownloadConvertBase.cs | 4 ++-- Source/FileLiberator/AudioDecodable.cs | 9 ++++++--- Source/FileLiberator/DownloadDecryptBook.cs | 2 -- 3 files changed, 8 insertions(+), 7 deletions(-) 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/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/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index 0f92ce8c..f6a70deb 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -121,8 +121,6 @@ namespace FileLiberator } } - - private async Task<bool> downloadAudiobookAsync(AudibleApi.Api api, Configuration config, DownloadOptions dlOptions) { var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(dlOptions.LibraryBookDto, dlOptions.OutputFormat.ToString().ToLower()); From 422c86345e37274a5037197f5e0121946c3f4374 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo <mbucari1@gmail.com> Date: Fri, 2 May 2025 14:50:33 -0600 Subject: [PATCH 7/8] Add logging --- Source/AaxDecrypter/NetworkFileStream.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs index f482bd4f..cea86b9b 100644 --- a/Source/AaxDecrypter/NetworkFileStream.cs +++ b/Source/AaxDecrypter/NetworkFileStream.cs @@ -185,14 +185,19 @@ namespace AaxDecrypter } 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}."); } } } From 313e3846c3f8b18e247e8084724804f5c3a96adf Mon Sep 17 00:00:00 2001 From: MBucari <mbucari1@gmail.com> Date: Fri, 2 May 2025 15:39:47 -0600 Subject: [PATCH 8/8] Remove AudioFormat from library book exporter (5f455182) --- Source/ApplicationServices/LibraryExporter.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Source/ApplicationServices/LibraryExporter.cs b/Source/ApplicationServices/LibraryExporter.cs index b299c1b2..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; } @@ -227,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), @@ -298,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)