This commit is contained in:
rmcrackan 2025-05-02 22:15:36 -04:00
commit 64a85b6aab
24 changed files with 352 additions and 262 deletions

View File

@ -103,8 +103,8 @@ namespace AaxDecrypter
OnInitialized(); OnInitialized();
OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged); OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged);
OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor ?? "[unknown]"); OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor);
OnRetrievedNarrators(AaxFile.AppleTags.Narrator ?? "[unknown]"); OnRetrievedNarrators(AaxFile.AppleTags.Narrator);
OnRetrievedCoverArt(AaxFile.AppleTags.Cover); OnRetrievedCoverArt(AaxFile.AppleTags.Cover);
return !IsCanceled; return !IsCanceled;

View File

@ -61,9 +61,6 @@ namespace AaxDecrypter
#region Constants #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 //Download memory buffer size
private const int DOWNLOAD_BUFF_SZ = 8 * 1024; private const int DOWNLOAD_BUFF_SZ = 8 * 1024;
@ -161,7 +158,7 @@ namespace AaxDecrypter
//Initiate connection with the first request block and //Initiate connection with the first request block and
//get the total content length before returning. //get the total content length before returning.
using var client = new HttpClient(); var client = new HttpClient();
var response = await RequestNextByteRangeAsync(client); var response = await RequestNextByteRangeAsync(client);
if (ContentLength != 0 && ContentLength != response.FileSize) if (ContentLength != 0 && ContentLength != response.FileSize)
@ -170,38 +167,59 @@ namespace AaxDecrypter
ContentLength = response.FileSize; ContentLength = response.FileSize;
_downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset); _downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Hand off the open request to the downloader to download and write data to file. //Hand off the client and the open request to the downloader to download and write data to file.
DownloadTask = Task.Run(() => DownloadLoopInternal(response), _cancellationSource.Token); 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 try
{ {
using var client = new HttpClient(); long startPosition = WritePosition;
while (WritePosition < ContentLength && !IsCancelled) while (WritePosition < ContentLength && !IsCancelled)
{ {
using var response = await RequestNextByteRangeAsync(client); try
await DownloadToFile(response); {
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 finally
{ {
_writeFile.Close(); _writeFile.Dispose();
blockResponse.Dispose();
client.Dispose();
} }
} }
private async Task<BlockResponse> RequestNextByteRangeAsync(HttpClient client) 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) foreach (var header in RequestHeaders)
request.Headers.Add(header.Key, header.Value); 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); var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancellationSource.Token);
@ -226,7 +244,7 @@ namespace AaxDecrypter
private async Task DownloadToFile(BlockResponse block) private async Task DownloadToFile(BlockResponse block)
{ {
var endPosition = WritePosition + block.BlockSize; 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 downloadPosition = WritePosition;
var nextFlush = downloadPosition + DATA_FLUSH_SZ; var nextFlush = downloadPosition + DATA_FLUSH_SZ;
@ -286,7 +304,6 @@ namespace AaxDecrypter
} }
finally finally
{ {
networkStream.Close();
_downloadedPiece.Set(); _downloadedPiece.Set();
OnUpdate(); OnUpdate();
} }

View File

@ -104,9 +104,6 @@ namespace ApplicationServices
[Name("Content Type")] [Name("Content Type")]
public string ContentType { get; set; } public string ContentType { get; set; }
[Name("Audio Format")]
public string AudioFormat { get; set; }
[Name("Language")] [Name("Language")]
public string Language { get; set; } public string Language { get; set; }
@ -152,7 +149,6 @@ namespace ApplicationServices
BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(), BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(),
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(), PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(),
ContentType = a.Book.ContentType.ToString(), ContentType = a.Book.ContentType.ToString(),
AudioFormat = a.Book.AudioFormat.ToString(),
Language = a.Book.Language, Language = a.Book.Language,
LastDownloaded = a.Book.UserDefinedItem.LastDownloaded, LastDownloaded = a.Book.UserDefinedItem.LastDownloaded,
LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "", LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "",
@ -228,7 +224,6 @@ namespace ApplicationServices
nameof(ExportDto.BookStatus), nameof(ExportDto.BookStatus),
nameof(ExportDto.PdfStatus), nameof(ExportDto.PdfStatus),
nameof(ExportDto.ContentType), nameof(ExportDto.ContentType),
nameof(ExportDto.AudioFormat),
nameof(ExportDto.Language), nameof(ExportDto.Language),
nameof(ExportDto.LastDownloaded), nameof(ExportDto.LastDownloaded),
nameof(ExportDto.LastDownloadedVersion), nameof(ExportDto.LastDownloadedVersion),
@ -299,7 +294,6 @@ namespace ApplicationServices
row.CreateCell(col++).SetCellValue(dto.BookStatus); row.CreateCell(col++).SetCellValue(dto.BookStatus);
row.CreateCell(col++).SetCellValue(dto.PdfStatus); row.CreateCell(col++).SetCellValue(dto.PdfStatus);
row.CreateCell(col++).SetCellValue(dto.ContentType); row.CreateCell(col++).SetCellValue(dto.ContentType);
row.CreateCell(col++).SetCellValue(dto.AudioFormat);
row.CreateCell(col++).SetCellValue(dto.Language); row.CreateCell(col++).SetCellValue(dto.Language);
if (dto.LastDownloaded.HasValue) if (dto.LastDownloaded.HasValue)

View File

@ -19,7 +19,6 @@ namespace DataLayer.Configurations
// //
entity.Ignore(nameof(Book.Authors)); entity.Ignore(nameof(Book.Authors));
entity.Ignore(nameof(Book.Narrators)); entity.Ignore(nameof(Book.Narrators));
entity.Ignore(nameof(Book.AudioFormat));
entity.Ignore(nameof(Book.TitleWithSubtitle)); entity.Ignore(nameof(Book.TitleWithSubtitle));
entity.Ignore(b => b.Categories); entity.Ignore(b => b.Categories);

View File

@ -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);
}
}

View File

@ -43,9 +43,9 @@ namespace DataLayer
public ContentType ContentType { get; private set; } public ContentType ContentType { get; private set; }
public string Locale { get; private set; } public string Locale { get; private set; }
internal AudioFormatEnum _audioFormat; //This field is now unused, however, there is little sense in adding a
//database migration to remove an unused field. Leave it for compatibility.
public AudioFormat AudioFormat { get => AudioFormat.FromEnum(_audioFormat); set => _audioFormat = value.ToEnum(); } internal long _audioFormat;
// mutable // mutable
public string PictureId { get; set; } public string PictureId { get; set; }

View File

@ -154,9 +154,6 @@ namespace DtoImporterService
// Update the book titles, since formatting can change // Update the book titles, since formatting can change
book.UpdateTitle(item.Title, item.Subtitle); 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 // set/update book-specific info which may have changed
if (item.PictureId is not null) if (item.PictureId is not null)
book.PictureId = item.PictureId; book.PictureId = item.PictureId;

View File

@ -19,6 +19,7 @@ namespace FileLiberator
protected void OnTitleDiscovered(object _, string title) protected void OnTitleDiscovered(object _, string title)
{ {
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(TitleDiscovered), Title = title }); Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(TitleDiscovered), Title = title });
if (title != null)
TitleDiscovered?.Invoke(this, title); TitleDiscovered?.Invoke(this, title);
} }
@ -26,6 +27,7 @@ namespace FileLiberator
protected void OnAuthorsDiscovered(object _, string authors) protected void OnAuthorsDiscovered(object _, string authors)
{ {
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(AuthorsDiscovered), Authors = authors }); Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(AuthorsDiscovered), Authors = authors });
if (authors != null)
AuthorsDiscovered?.Invoke(this, authors); AuthorsDiscovered?.Invoke(this, authors);
} }
@ -33,6 +35,7 @@ namespace FileLiberator
protected void OnNarratorsDiscovered(object _, string narrators) protected void OnNarratorsDiscovered(object _, string narrators)
{ {
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(NarratorsDiscovered), Narrators = narrators }); Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(NarratorsDiscovered), Narrators = narrators });
if (narrators != null)
NarratorsDiscovered?.Invoke(this, narrators); NarratorsDiscovered?.Invoke(this, narrators);
} }

View File

@ -39,8 +39,8 @@ namespace FileLiberator
/// Path: in progress directory. /// Path: in progress directory.
/// File name: final file name. /// File name: final file name.
/// </summary> /// </summary>
public static string GetInProgressFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension) public static string GetInProgressFilename(this AudioFileStorage _, LibraryBookDto libraryBook, string extension)
=> Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.DecryptInProgressDirectory, extension, returnFirstExisting: true); => Templates.File.GetFilename(libraryBook, AudibleFileStorage.DecryptInProgressDirectory, extension, returnFirstExisting: true);
/// <summary> /// <summary>
/// PDF: audio file does not exist /// PDF: audio file does not exist
@ -48,6 +48,12 @@ namespace FileLiberator
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension) public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
=> Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.BooksDirectory, extension); => Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.BooksDirectory, extension);
/// <summary>
/// PDF: audio file does not exist
/// </summary>
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBookDto dto, string extension)
=> Templates.File.GetFilename(dto, AudibleFileStorage.BooksDirectory, extension);
/// <summary> /// <summary>
/// PDF: audio file already exists /// PDF: audio file already exists
/// </summary> /// </summary>

View File

@ -47,13 +47,18 @@ namespace FileLiberator
if (libraryBook.Book.Audio_Exists()) if (libraryBook.Book.Audio_Exists())
return new StatusHandler { "Cannot find decrypt. Final audio file already 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; bool success = false;
try try
{ {
FilePathCache.Inserted += FilePathCache_Inserted; FilePathCache.Inserted += FilePathCache_Inserted;
FilePathCache.Removed += FilePathCache_Removed; FilePathCache.Removed += FilePathCache_Removed;
success = await downloadAudiobookAsync(libraryBook); success = await downloadAudiobookAsync(api, config, downloadOptions);
} }
finally finally
{ {
@ -78,12 +83,12 @@ namespace FileLiberator
var finalStorageDir = getDestinationDirectory(libraryBook); var finalStorageDir = getDestinationDirectory(libraryBook);
var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries)); var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries));
Task[] finalTasks = new[] Task[] finalTasks =
{ [
Task.Run(() => downloadCoverArt(libraryBook)), Task.Run(() => downloadCoverArt(downloadOptions)),
moveFilesTask, moveFilesTask,
Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir)) Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir))
}; ];
try try
{ {
@ -116,16 +121,9 @@ namespace FileLiberator
} }
} }
private async Task<bool> downloadAudiobookAsync(LibraryBook libraryBook) private async Task<bool> downloadAudiobookAsync(AudibleApi.Api api, Configuration config, DownloadOptions dlOptions)
{ {
var config = Configuration.Instance; var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(dlOptions.LibraryBookDto, dlOptions.OutputFormat.ToString().ToLower());
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 cacheDir = AudibleFileStorage.DownloadsInProgressDirectory; var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
if (dlOptions.DrmType is not DrmType.Adrm and not DrmType.Widevine) if (dlOptions.DrmType is not DrmType.Adrm and not DrmType.Widevine)
@ -149,7 +147,7 @@ namespace FileLiberator
abDownloader.RetrievedAuthors += OnAuthorsDiscovered; abDownloader.RetrievedAuthors += OnAuthorsDiscovered;
abDownloader.RetrievedNarrators += OnNarratorsDiscovered; abDownloader.RetrievedNarrators += OnNarratorsDiscovered;
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt; abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path); abDownloader.FileCreated += (_, path) => OnFileCreated(dlOptions.LibraryBook, path);
// REAL WORK DONE HERE // REAL WORK DONE HERE
var success = await abDownloader.RunAsync(); 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 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.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(dlOptions.ContentMetadata.ChapterInfo));
item.SourceJson.Add(nameof(ContentMetadata.ContentReference), Newtonsoft.Json.Linq.JObject.FromObject(dlOptions.ContentMetadata.ContentReference)); item.SourceJson.Add(nameof(ContentMetadata.ContentReference), Newtonsoft.Json.Linq.JObject.FromObject(dlOptions.ContentMetadata.ContentReference));
File.WriteAllText(metadataFile, item.SourceJson.ToString()); File.WriteAllText(metadataFile, item.SourceJson.ToString());
OnFileCreated(libraryBook, metadataFile); OnFileCreated(dlOptions.LibraryBook, metadataFile);
} }
return success; return success;
} }
@ -280,7 +278,7 @@ namespace FileLiberator
private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable<FilePathCache.CacheEntry> entries) private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable<FilePathCache.CacheEntry> entries)
=> entries.FirstOrDefault(f => f.FileType == FileType.Audio); => entries.FirstOrDefault(f => f.FileType == FileType.Audio);
private static void downloadCoverArt(LibraryBook libraryBook) private static void downloadCoverArt(DownloadOptions options)
{ {
if (!Configuration.Instance.DownloadCoverArt) return; if (!Configuration.Instance.DownloadCoverArt) return;
@ -288,24 +286,24 @@ namespace FileLiberator
try try
{ {
var destinationDir = getDestinationDirectory(libraryBook); var destinationDir = getDestinationDirectory(options.LibraryBook);
coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, ".jpg"); coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(options.LibraryBookDto, ".jpg");
coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath)); coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath));
if (File.Exists(coverPath)) if (File.Exists(coverPath))
FileUtility.SaferDelete(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) if (picBytes.Length > 0)
{ {
File.WriteAllBytes(coverPath, picBytes); File.WriteAllBytes(coverPath, picBytes);
SetFileTime(libraryBook, coverPath); SetFileTime(options.LibraryBook, coverPath);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
//Failure to download cover art should not be considered a failure to download the book //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.");
} }
} }
} }

View File

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable #nullable enable
@ -23,7 +24,7 @@ public partial class DownloadOptions
/// <summary> /// <summary>
/// Initiate an audiobook download from the audible api. /// Initiate an audiobook download from the audible api.
/// </summary> /// </summary>
public static async Task<DownloadOptions> InitiateDownloadAsync(Api api, LibraryBook libraryBook, Configuration config) public static async Task<DownloadOptions> InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook)
{ {
var license = await ChooseContent(api, libraryBook, config); var license = await ChooseContent(api, libraryBook, config);
var options = BuildDownloadOptions(libraryBook, config, license); var options = BuildDownloadOptions(libraryBook, config, license);
@ -33,42 +34,40 @@ public partial class DownloadOptions
private static async Task<ContentLicense> ChooseContent(Api api, LibraryBook libraryBook, Configuration config) private static async Task<ContentLicense> ChooseContent(Api api, LibraryBook libraryBook, Configuration config)
{ {
var cdm = await Cdm.GetCdmAsync();
var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High; var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High;
ContentLicense? contentLic = null; if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm)
ContentLicense? fallback = null; return await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
ContentLicense? contentLic = null, fallback = null;
if (cdm is null)
{
//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);
}
else
{
var spatial = config.FileDownloadQuality is Configuration.DownloadQuality.Spatial;
try try
{ {
//try to request a widevine content license using the user's spatial audio settings
var codecChoice = config.SpatialAudioCodec switch var codecChoice = config.SpatialAudioCodec switch
{ {
Configuration.SpatialCodec.EC_3 => Ec3Codec, Configuration.SpatialCodec.EC_3 => Ec3Codec,
Configuration.SpatialCodec.AC_4 => Ac4Codec, Configuration.SpatialCodec.AC_4 => Ac4Codec,
_ => throw new NotSupportedException($"Unknown value for {nameof(config.SpatialAudioCodec)}") _ => throw new NotSupportedException($"Unknown value for {nameof(config.SpatialAudioCodec)}")
}; };
contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality, ChapterTitlesType.Tree, DrmType.Widevine, spatial, codecChoice);
contentLic
= await api.GetDownloadLicenseAsync(
libraryBook.Book.AudibleProductId,
dlQuality,
ChapterTitlesType.Tree,
DrmType.Widevine,
config.RequestSpatial,
codecChoice);
} }
catch (Exception ex) catch (Exception ex)
{ {
Serilog.Log.Logger.Error(ex, "Failed to request a Widevine license."); 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 is null) if (!contentLic.ContentMetadata.ContentReference.IsSpatial && contentLic.DrmType != DrmType.Adrm)
{
//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 We got a widevine license and we have a Cdm, but we still need to decide if we WANT the file
@ -86,8 +85,8 @@ public partial class DownloadOptions
To decide which file we want, use this simple rule: if files are different codecs and 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. Widevine is significantly larger, use Widevine. Otherwise use ADRM.
*/ */
fallback = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality); fallback = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
var wvCr = contentLic.ContentMetadata.ContentReference; var wvCr = contentLic.ContentMetadata.ContentReference;
@ -100,14 +99,13 @@ public partial class DownloadOptions
contentLic = fallback; contentLic = fallback;
} }
} }
}
if (contentLic.DrmType == DrmType.Widevine && cdm is not null) if (contentLic.DrmType == DrmType.Widevine)
{ {
try try
{ {
using var client = new HttpClient(); 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()); var dash = new MpegDash(mpdResponse.Content.ReadAsStream());
if (!dash.TryGetUri(new Uri(contentLic.LicenseResponse), out var contentUri)) 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()), Key = Convert.ToHexStringLower(keys[0].Kid.ToByteArray()),
Iv = Convert.ToHexStringLower(keys[0].Key) Iv = Convert.ToHexStringLower(keys[0].Key)
}; };
} }
catch 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 : contentLic.DrmType is DrmType.Adrm && contentLic.Voucher?.Key.Length == 32 && contentLic.Voucher?.Iv.Length == 32 ? AAXClean.FileType.Aaxc
: null; : 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) var dlOptions = new DownloadOptions(config, libraryBook, contentLic.ContentMetadata.ContentUrl?.OfflineUrl)
{ {
AudibleKey = contentLic.Voucher?.Key, AudibleKey = contentLic.Voucher?.Key,
@ -176,6 +170,14 @@ public partial class DownloadOptions
RuntimeLength = TimeSpan.FromMilliseconds(contentLic.ContentMetadata.ChapterInfo.RuntimeLengthMs), 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 titleConcat = config.CombineNestedChapterTitles ? ": " : null;
var chapters var chapters
= flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters, titleConcat) = flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters, titleConcat)
@ -202,6 +204,43 @@ public partial class DownloadOptions
return dlOptions; return dlOptions;
} }
/// <summary>
/// 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.
/// </summary>
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) public static LameConfig GetLameOptions(Configuration config)
{ {
LameConfig lameConfig = new() LameConfig lameConfig = new()
@ -359,4 +398,9 @@ public partial class DownloadOptions
static double RelativePercentDifference(long num1, long num2) static double RelativePercentDifference(long num1, long num2)
=> Math.Abs(num1 - num2) / (double)(num1 + 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();
} }

View File

@ -55,9 +55,6 @@ namespace FileLiberator
IsPodcastParent = libraryBook.Book.IsEpisodeParent(), IsPodcastParent = libraryBook.Book.IsEpisodeParent(),
IsPodcast = libraryBook.Book.IsEpisodeChild() || 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 Language = libraryBook.Book.Language
}; };
} }

View File

@ -43,14 +43,40 @@
<controls:WheelComboBox <controls:WheelComboBox
Margin="5,0,0,0" Margin="5,0,0,0"
Grid.Column="1" Grid.Column="1"
SelectionChanged="Quality_SelectionChanged"
ItemsSource="{CompiledBinding DownloadQualities}" ItemsSource="{CompiledBinding DownloadQualities}"
SelectedItem="{CompiledBinding FileDownloadQuality}"/> SelectedItem="{CompiledBinding FileDownloadQuality}"/>
</Grid> </Grid>
<Grid ColumnDefinitions="*,Auto" Margin="0,5,0,0"
IsEnabled="{CompiledBinding SpatialSelected}" <Grid ColumnDefinitions="*,*">
<CheckBox
IsChecked="{CompiledBinding UseWidevine, Mode=TwoWay}"
IsCheckedChanged="UseWidevine_IsCheckedChanged"
ToolTip.Tip="{CompiledBinding UseWidevineTip}">
<TextBlock Text="{CompiledBinding UseWidevineText}" />
</CheckBox>
<CheckBox
Grid.Column="1"
HorizontalAlignment="Right"
ToolTip.Tip="{CompiledBinding RequestSpatialTip}"
IsEnabled="{CompiledBinding UseWidevine}"
IsChecked="{CompiledBinding RequestSpatial, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding RequestSpatialText}" />
</CheckBox>
</Grid>
<Grid ColumnDefinitions="*,Auto"
ToolTip.Tip="{CompiledBinding SpatialAudioCodecTip}"> ToolTip.Tip="{CompiledBinding SpatialAudioCodecTip}">
<Grid.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<MultiBinding.Bindings>
<CompiledBinding Path="UseWidevine"/>
<CompiledBinding Path="RequestSpatial"/>
</MultiBinding.Bindings>
</MultiBinding>
</Grid.IsEnabled>
<TextBlock <TextBlock
VerticalAlignment="Center" VerticalAlignment="Center"
@ -63,6 +89,11 @@
SelectedItem="{CompiledBinding SpatialAudioCodec}"/> SelectedItem="{CompiledBinding SpatialAudioCodec}"/>
</Grid> </Grid>
<CheckBox IsChecked="{CompiledBinding CreateCueSheet, Mode=TwoWay}"> <CheckBox IsChecked="{CompiledBinding CreateCueSheet, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding CreateCueSheetText}" /> <TextBlock Text="{CompiledBinding CreateCueSheetText}" />
</CheckBox> </CheckBox>

View File

@ -22,21 +22,21 @@ namespace LibationAvalonia.Controls.Settings
} }
} }
private async void UseWidevine_IsCheckedChanged(object sender, Avalonia.Interactivity.RoutedEventArgs e)
public async void Quality_SelectionChanged(object sender, SelectionChangedEventArgs e)
{ {
if (_viewModel.SpatialSelected) if (sender is CheckBox cbox && cbox.IsChecked is true)
{ {
using var accounts = AudibleApiStorage.GetAccountsSettingsPersister(); using var accounts = AudibleApiStorage.GetAccountsSettingsPersister();
if (!accounts.AccountsSettings.Accounts.Any(a => a.IdentityTokens.DeviceType == AudibleApi.Resources.DeviceType)) if (!accounts.AccountsSettings.Accounts.Any(a => a.IdentityTokens.DeviceType == AudibleApi.Resources.DeviceType))
{ {
await MessageBox.Show(VisualRoot as Window, if (VisualRoot is Window parent)
"Your must remove account(s) from Libation and then re-add them to enable spatial audiobook downloads.", await MessageBox.Show(parent,
"Spatial Audio Unavailable", "Your must remove account(s) from Libation and then re-add them to enable widwvine content.",
"Widevine Content Unavailable",
MessageBoxButtons.OK); MessageBoxButtons.OK);
_viewModel.FileDownloadQuality = _viewModel.DownloadQualities[1]; _viewModel.UseWidevine = false;
} }
} }
} }

View File

@ -114,7 +114,6 @@ Title: {title}
Author(s): {Book.AuthorNames()} Author(s): {Book.AuthorNames()}
Narrator(s): {Book.NarratorNames()} Narrator(s): {Book.NarratorNames()}
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")} Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
Audio Bitrate: {Book.AudioFormat}
Category: {string.Join(", ", Book.LowestCategoryNames())} Category: {string.Join(", ", Book.LowestCategoryNames())}
Purchase Date: {libraryBook.DateAdded:d} Purchase Date: {libraryBook.DateAdded:d}
Language: {Book.Language} Language: {Book.Language}

View File

@ -67,6 +67,8 @@ namespace LibationAvalonia.ViewModels.Settings
FileDownloadQuality = DownloadQualities.SingleOrDefault(s => s.Value == config.FileDownloadQuality) ?? DownloadQualities[0]; FileDownloadQuality = DownloadQualities.SingleOrDefault(s => s.Value == config.FileDownloadQuality) ?? DownloadQualities[0];
SelectedSampleRate = SampleRates.SingleOrDefault(s => s.Value == config.MaxSampleRate) ?? SampleRates[0]; SelectedSampleRate = SampleRates.SingleOrDefault(s => s.Value == config.MaxSampleRate) ?? SampleRates[0];
SelectedEncoderQuality = config.LameEncoderQuality; SelectedEncoderQuality = config.LameEncoderQuality;
UseWidevine = config.UseWidevine;
RequestSpatial = config.RequestSpatial;
} }
public void SaveSettings(Configuration config) public void SaveSettings(Configuration config)
@ -96,12 +98,13 @@ namespace LibationAvalonia.ViewModels.Settings
config.MaxSampleRate = SelectedSampleRate?.Value ?? config.MaxSampleRate; config.MaxSampleRate = SelectedSampleRate?.Value ?? config.MaxSampleRate;
config.FileDownloadQuality = FileDownloadQuality?.Value ?? config.FileDownloadQuality; config.FileDownloadQuality = FileDownloadQuality?.Value ?? config.FileDownloadQuality;
config.SpatialAudioCodec = SpatialAudioCodec?.Value ?? config.SpatialAudioCodec; config.SpatialAudioCodec = SpatialAudioCodec?.Value ?? config.SpatialAudioCodec;
config.UseWidevine = UseWidevine;
config.RequestSpatial = RequestSpatial;
} }
public AvaloniaList<EnumDisplay<Configuration.DownloadQuality>> DownloadQualities { get; } = new([ public AvaloniaList<EnumDisplay<Configuration.DownloadQuality>> DownloadQualities { get; } = new([
new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.Normal), new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.Normal),
new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.High), new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.High),
new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.Spatial, "Spatial (if available)"),
]); ]);
public AvaloniaList<EnumDisplay<Configuration.SpatialCodec>> SpatialAudioCodecs { get; } = new([ public AvaloniaList<EnumDisplay<Configuration.SpatialCodec>> SpatialAudioCodecs { get; } = new([
new EnumDisplay<Configuration.SpatialCodec>(Configuration.SpatialCodec.EC_3, "Dolby Digital Plus (E-AC-3)"), new EnumDisplay<Configuration.SpatialCodec>(Configuration.SpatialCodec.EC_3, "Dolby Digital Plus (E-AC-3)"),
@ -109,6 +112,10 @@ namespace LibationAvalonia.ViewModels.Settings
]); ]);
public AvaloniaList<Configuration.ClipBookmarkFormat> ClipBookmarkFormats { get; } = new(Enum<Configuration.ClipBookmarkFormat>.GetValues()); public AvaloniaList<Configuration.ClipBookmarkFormat> ClipBookmarkFormats { get; } = new(Enum<Configuration.ClipBookmarkFormat>.GetValues());
public string FileDownloadQualityText { get; } = Configuration.GetDescription(nameof(Configuration.FileDownloadQuality)); 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 SpatialAudioCodecText { get; } = Configuration.GetDescription(nameof(Configuration.SpatialAudioCodec));
public string SpatialAudioCodecTip { get; } = Configuration.GetHelpText(nameof(Configuration.SpatialAudioCodec)); public string SpatialAudioCodecTip { get; } = Configuration.GetHelpText(nameof(Configuration.SpatialAudioCodec));
public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet)); 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 string RetainAaxFileTip => Configuration.GetHelpText(nameof(RetainAaxFile));
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); } public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }
public bool SpatialSelected { get; private set; }
private EnumDisplay<Configuration.DownloadQuality>? _fileDownloadQuality; private bool _useWidevine;
public EnumDisplay<Configuration.DownloadQuality> FileDownloadQuality private bool _requestSpatial;
{ public bool UseWidevine { get => _useWidevine; set => this.RaiseAndSetIfChanged(ref _useWidevine, value); }
get => _fileDownloadQuality ?? DownloadQualities[0]; public bool RequestSpatial { get => _requestSpatial; set => this.RaiseAndSetIfChanged(ref _requestSpatial, value); }
set
{ public EnumDisplay<Configuration.DownloadQuality> FileDownloadQuality { get; set; }
SpatialSelected = value?.Value == Configuration.DownloadQuality.Spatial;
this.RaiseAndSetIfChanged(ref _fileDownloadQuality, value);
this.RaisePropertyChanged(nameof(SpatialSelected));
}
}
public EnumDisplay<Configuration.SpatialCodec> SpatialAudioCodec { get; set; } public EnumDisplay<Configuration.SpatialCodec> SpatialAudioCodec { get; set; }
public Configuration.ClipBookmarkFormat ClipBookmarkFormat { get; set; } public Configuration.ClipBookmarkFormat ClipBookmarkFormat { get; set; }
public bool MergeOpeningAndEndCredits { get; set; } public bool MergeOpeningAndEndCredits { get; set; }

View File

@ -89,6 +89,24 @@ namespace LibationFileManager
AC-4 cannot be converted to MP3. 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(); .AsReadOnly();

View File

@ -246,8 +246,7 @@ namespace LibationFileManager
public enum DownloadQuality public enum DownloadQuality
{ {
High, High,
Normal, Normal
Spatial
} }
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
@ -257,6 +256,12 @@ namespace LibationFileManager
AC_4 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:")] [Description("Spatial audio codec:")]
public SpatialCodec SpatialAudioCodec { get => GetNonString(defaultValue: SpatialCodec.EC_3); set => SetNonString(value); } public SpatialCodec SpatialAudioCodec { get => GetNonString(defaultValue: SpatialCodec.EC_3); set => SetNonString(value); }

View File

@ -27,9 +27,10 @@ public class BookDto
public bool IsPodcastParent { get; set; } public bool IsPodcastParent { get; set; }
public bool IsPodcast { get; set; } public bool IsPodcast { get; set; }
public int BitRate { get; set; } public int? BitRate { get; set; }
public int SampleRate { get; set; } public int? SampleRate { get; set; }
public int Channels { get; set; } public int? Channels { get; set; }
public string? Codec { get; set; }
public DateTime FileDate { get; set; } = DateTime.Now; public DateTime FileDate { get; set; } = DateTime.Now;
public DateTime? DatePublished { get; set; } public DateTime? DatePublished { get; set; }
public string? Language { get; set; } public string? Language { get; set; }

View File

@ -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 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 FirstSeries { get; } = new TemplateTags("first series", "First series");
public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series (alias for <first series[{#}]>"); public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series (alias for <first series[{#}]>");
public static TemplateTags Bitrate { get; } = new TemplateTags("bitrate", "File's orig. bitrate"); public static TemplateTags Bitrate { get; } = new TemplateTags("bitrate", "Audiobook's source bitrate");
public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "File's orig. sample rate"); public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "Audiobook's source sample rate");
public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels"); 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 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 AccountNickname { get; } = new TemplateTags("account nickname", "Audible account nickname of this book");
public static TemplateTags Locale { get; } = new("locale", "Region/country"); public static TemplateTags Locale { get; } = new("locale", "Region/country");

View File

@ -271,9 +271,6 @@ namespace LibationFileManager.Templates
{ TemplateTags.Language, lb => lb.Language }, { TemplateTags.Language, lb => lb.Language },
//Don't allow formatting of LanguageShort //Don't allow formatting of LanguageShort
{ TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort }, { 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.Account, lb => lb.Account },
{ TemplateTags.AccountNickname, lb => lb.AccountNickname }, { TemplateTags.AccountNickname, lb => lb.AccountNickname },
{ TemplateTags.Locale, lb => lb.Locale }, { TemplateTags.Locale, lb => lb.Locale },
@ -283,6 +280,15 @@ namespace LibationFileManager.Templates
{ TemplateTags.FileDate, lb => lb.FileDate }, { TemplateTags.FileDate, lb => lb.FileDate },
}; };
private static readonly PropertyTagCollection<LibraryBookDto> 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<TagCollection> chapterPropertyTags = new() private static readonly List<TagCollection> chapterPropertyTags = new()
{ {
new PropertyTagCollection<LibraryBookDto>(caseSensative: true, StringFormatter) new PropertyTagCollection<LibraryBookDto>(caseSensative: true, StringFormatter)
@ -376,8 +382,7 @@ namespace LibationFileManager.Templates
public static string Name { get; } = "Folder Template"; public static string Name { get; } = "Folder Template";
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? ""; public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? "";
public static string DefaultTemplate { get; } = "<title short> [<id>]"; public static string DefaultTemplate { get; } = "<title short> [<id>]";
public static IEnumerable<TagCollection> TagCollections public static IEnumerable<TagCollection> TagCollections { get; } = [filePropertyTags, conditionalTags, folderConditionalTags];
=> new TagCollection[] { filePropertyTags, conditionalTags, folderConditionalTags };
public override IEnumerable<string> Errors public override IEnumerable<string> Errors
=> TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ERROR_FULL_PATH_IS_INVALID) : base.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 Name { get; } = "File Template";
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate)) ?? ""; public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate)) ?? "";
public static string DefaultTemplate { get; } = "<title> [<id>]"; 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 public class ChapterFileTemplate : Templates, ITemplate
@ -404,7 +409,8 @@ namespace LibationFileManager.Templates
public static string Name { get; } = "Chapter File Template"; public static string Name { get; } = "Chapter File Template";
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)) ?? ""; public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)) ?? "";
public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>"; 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 public override IEnumerable<string> Warnings
=> NamingTemplate.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName)) => NamingTemplate.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName))

View File

@ -49,7 +49,6 @@ Title: {title}
Author(s): {Book.AuthorNames()} Author(s): {Book.AuthorNames()}
Narrator(s): {Book.NarratorNames()} Narrator(s): {Book.NarratorNames()}
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")} Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
Audio Bitrate: {Book.AudioFormat}
Category: {string.Join(", ", Book.LowestCategoryNames())} Category: {string.Join(", ", Book.LowestCategoryNames())}
Purchase Date: {_libraryBook.DateAdded:d} Purchase Date: {_libraryBook.DateAdded:d}
Language: {Book.Language} Language: {Book.Language}

View File

@ -23,6 +23,8 @@ namespace LibationWinForms.Dialogs
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio)); this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged)); this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
this.moveMoovAtomCbox.Text = desc(nameof(config.MoveMoovToBeginning)); 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)); this.spatialCodecLbl.Text = desc(nameof(config.SpatialAudioCodec));
toolTip.SetToolTip(combineNestedChapterTitlesCbox, Configuration.GetHelpText(nameof(config.CombineNestedChapterTitles))); 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(mergeOpeningEndCreditsCbox, Configuration.GetHelpText(nameof(config.MergeOpeningAndEndCredits)));
toolTip.SetToolTip(retainAaxFileCbox, Configuration.GetHelpText(nameof(config.RetainAaxFile))); toolTip.SetToolTip(retainAaxFileCbox, Configuration.GetHelpText(nameof(config.RetainAaxFile)));
toolTip.SetToolTip(stripAudibleBrandingCbox, Configuration.GetHelpText(nameof(config.StripAudibleBrandAudio))); 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(spatialCodecLbl, Configuration.GetHelpText(nameof(config.SpatialAudioCodec)));
toolTip.SetToolTip(spatialAudioCodecCb, 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.Normal),
new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.High), new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.High),
new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.Spatial, "Spatial (if available)"),
]); ]);
spatialAudioCodecCb.Items.AddRange( spatialAudioCodecCb.Items.AddRange(
@ -76,6 +79,8 @@ namespace LibationWinForms.Dialogs
downloadClipsBookmarksCbox.Checked = config.DownloadClipsBookmarks; downloadClipsBookmarksCbox.Checked = config.DownloadClipsBookmarks;
fileDownloadQualityCb.SelectedItem = config.FileDownloadQuality; fileDownloadQualityCb.SelectedItem = config.FileDownloadQuality;
spatialAudioCodecCb.SelectedItem = config.SpatialAudioCodec; spatialAudioCodecCb.SelectedItem = config.SpatialAudioCodec;
useWidevineCbox.Checked = config.UseWidevine;
requestSpatialCbox.Checked = config.RequestSpatial;
clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat; clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat;
retainAaxFileCbox.Checked = config.RetainAaxFile; retainAaxFileCbox.Checked = config.RetainAaxFile;
@ -118,6 +123,8 @@ namespace LibationWinForms.Dialogs
config.DownloadCoverArt = downloadCoverArtCbox.Checked; config.DownloadCoverArt = downloadCoverArtCbox.Checked;
config.DownloadClipsBookmarks = downloadClipsBookmarksCbox.Checked; config.DownloadClipsBookmarks = downloadClipsBookmarksCbox.Checked;
config.FileDownloadQuality = ((EnumDisplay<Configuration.DownloadQuality>)fileDownloadQualityCb.SelectedItem).Value; 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.SpatialAudioCodec = ((EnumDisplay<Configuration.SpatialCodec>)spatialAudioCodecCb.SelectedItem).Value;
config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem; config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem;
config.RetainAaxFile = retainAaxFileCbox.Checked; config.RetainAaxFile = retainAaxFileCbox.Checked;
@ -140,7 +147,6 @@ namespace LibationWinForms.Dialogs
config.ChapterTitleTemplate = chapterTitleTemplateTb.Text; config.ChapterTitleTemplate = chapterTitleTemplateTb.Text;
} }
private void downloadClipsBookmarksCbox_CheckedChanged(object sender, EventArgs e) private void downloadClipsBookmarksCbox_CheckedChanged(object sender, EventArgs e)
{ {
clipsBookmarksFormatCb.Enabled = downloadClipsBookmarksCbox.Checked; 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(); using var accounts = AudibleApiStorage.GetAccountsSettingsPersister();
if (!accounts.AccountsSettings.Accounts.Any(a => a.IdentityTokens.DeviceType == AudibleApi.Resources.DeviceType)) if (!accounts.AccountsSettings.Accounts.Any(a => a.IdentityTokens.DeviceType == AudibleApi.Resources.DeviceType))
{ {
MessageBox.Show(this, MessageBox.Show(this,
"Your must remove account(s) from Libation and then re-add them to enable spatial audiobook downloads.", "Your must remove account(s) from Libation and then re-add them to enable widwvine content.",
"Spatial Audio Unavailable", "Widevine Content Unavailable",
MessageBoxButtons.OK); MessageBoxButtons.OK);
fileDownloadQualityCb.SelectedItem = Configuration.DownloadQuality.High; useWidevineCbox.Checked = false;
return; return;
} }
} }
requestSpatialCbox.Enabled = useWidevineCbox.Checked;
spatialCodecLbl.Enabled = spatialAudioCodecCb.Enabled = useWidevineCbox.Checked && requestSpatialCbox.Checked;
}
spatialCodecLbl.Enabled = spatialAudioCodecCb.Enabled = selectedSpatial;
}
} }
} }

View File

@ -84,6 +84,8 @@
folderTemplateTb = new System.Windows.Forms.TextBox(); folderTemplateTb = new System.Windows.Forms.TextBox();
folderTemplateLbl = new System.Windows.Forms.Label(); folderTemplateLbl = new System.Windows.Forms.Label();
tab4AudioFileOptions = new System.Windows.Forms.TabPage(); tab4AudioFileOptions = new System.Windows.Forms.TabPage();
requestSpatialCbox = new System.Windows.Forms.CheckBox();
useWidevineCbox = new System.Windows.Forms.CheckBox();
spatialAudioCodecCb = new System.Windows.Forms.ComboBox(); spatialAudioCodecCb = new System.Windows.Forms.ComboBox();
spatialCodecLbl = new System.Windows.Forms.Label(); spatialCodecLbl = new System.Windows.Forms.Label();
moveMoovAtomCbox = new System.Windows.Forms.CheckBox(); moveMoovAtomCbox = new System.Windows.Forms.CheckBox();
@ -306,7 +308,7 @@
allowLibationFixupCbox.AutoSize = true; allowLibationFixupCbox.AutoSize = true;
allowLibationFixupCbox.Checked = true; allowLibationFixupCbox.Checked = true;
allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked; 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.Name = "allowLibationFixupCbox";
allowLibationFixupCbox.Size = new System.Drawing.Size(162, 19); allowLibationFixupCbox.Size = new System.Drawing.Size(162, 19);
allowLibationFixupCbox.TabIndex = 11; allowLibationFixupCbox.TabIndex = 11;
@ -772,6 +774,8 @@
// tab4AudioFileOptions // tab4AudioFileOptions
// //
tab4AudioFileOptions.AutoScroll = true; tab4AudioFileOptions.AutoScroll = true;
tab4AudioFileOptions.Controls.Add(requestSpatialCbox);
tab4AudioFileOptions.Controls.Add(useWidevineCbox);
tab4AudioFileOptions.Controls.Add(spatialAudioCodecCb); tab4AudioFileOptions.Controls.Add(spatialAudioCodecCb);
tab4AudioFileOptions.Controls.Add(spatialCodecLbl); tab4AudioFileOptions.Controls.Add(spatialCodecLbl);
tab4AudioFileOptions.Controls.Add(moveMoovAtomCbox); tab4AudioFileOptions.Controls.Add(moveMoovAtomCbox);
@ -798,11 +802,38 @@
tab4AudioFileOptions.Text = "Audio File Options"; tab4AudioFileOptions.Text = "Audio File Options";
tab4AudioFileOptions.UseVisualStyleBackColor = true; 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
// //
spatialAudioCodecCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; spatialAudioCodecCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
spatialAudioCodecCb.FormattingEnabled = true; 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.Margin = new System.Windows.Forms.Padding(3, 3, 5, 3);
spatialAudioCodecCb.Name = "spatialAudioCodecCb"; spatialAudioCodecCb.Name = "spatialAudioCodecCb";
spatialAudioCodecCb.Size = new System.Drawing.Size(173, 23); spatialAudioCodecCb.Size = new System.Drawing.Size(173, 23);
@ -811,7 +842,7 @@
// spatialCodecLbl // spatialCodecLbl
// //
spatialCodecLbl.AutoSize = true; spatialCodecLbl.AutoSize = true;
spatialCodecLbl.Location = new System.Drawing.Point(19, 37); spatialCodecLbl.Location = new System.Drawing.Point(19, 62);
spatialCodecLbl.Name = "spatialCodecLbl"; spatialCodecLbl.Name = "spatialCodecLbl";
spatialCodecLbl.Size = new System.Drawing.Size(143, 15); spatialCodecLbl.Size = new System.Drawing.Size(143, 15);
spatialCodecLbl.TabIndex = 24; spatialCodecLbl.TabIndex = 24;
@ -836,7 +867,6 @@
fileDownloadQualityCb.Name = "fileDownloadQualityCb"; fileDownloadQualityCb.Name = "fileDownloadQualityCb";
fileDownloadQualityCb.Size = new System.Drawing.Size(130, 23); fileDownloadQualityCb.Size = new System.Drawing.Size(130, 23);
fileDownloadQualityCb.TabIndex = 1; fileDownloadQualityCb.TabIndex = 1;
fileDownloadQualityCb.SelectedIndexChanged += fileDownloadQualityCb_SelectedIndexChanged;
// //
// fileDownloadQualityLbl // fileDownloadQualityLbl
// //
@ -851,7 +881,7 @@
// combineNestedChapterTitlesCbox // combineNestedChapterTitlesCbox
// //
combineNestedChapterTitlesCbox.AutoSize = true; combineNestedChapterTitlesCbox.AutoSize = true;
combineNestedChapterTitlesCbox.Location = new System.Drawing.Point(19, 181); combineNestedChapterTitlesCbox.Location = new System.Drawing.Point(19, 206);
combineNestedChapterTitlesCbox.Name = "combineNestedChapterTitlesCbox"; combineNestedChapterTitlesCbox.Name = "combineNestedChapterTitlesCbox";
combineNestedChapterTitlesCbox.Size = new System.Drawing.Size(217, 19); combineNestedChapterTitlesCbox.Size = new System.Drawing.Size(217, 19);
combineNestedChapterTitlesCbox.TabIndex = 10; combineNestedChapterTitlesCbox.TabIndex = 10;
@ -862,7 +892,7 @@
// //
clipsBookmarksFormatCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; clipsBookmarksFormatCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
clipsBookmarksFormatCb.FormattingEnabled = true; clipsBookmarksFormatCb.FormattingEnabled = true;
clipsBookmarksFormatCb.Location = new System.Drawing.Point(285, 107); clipsBookmarksFormatCb.Location = new System.Drawing.Point(285, 132);
clipsBookmarksFormatCb.Name = "clipsBookmarksFormatCb"; clipsBookmarksFormatCb.Name = "clipsBookmarksFormatCb";
clipsBookmarksFormatCb.Size = new System.Drawing.Size(67, 23); clipsBookmarksFormatCb.Size = new System.Drawing.Size(67, 23);
clipsBookmarksFormatCb.TabIndex = 6; clipsBookmarksFormatCb.TabIndex = 6;
@ -870,7 +900,7 @@
// downloadClipsBookmarksCbox // downloadClipsBookmarksCbox
// //
downloadClipsBookmarksCbox.AutoSize = true; downloadClipsBookmarksCbox.AutoSize = true;
downloadClipsBookmarksCbox.Location = new System.Drawing.Point(19, 109); downloadClipsBookmarksCbox.Location = new System.Drawing.Point(19, 134);
downloadClipsBookmarksCbox.Name = "downloadClipsBookmarksCbox"; downloadClipsBookmarksCbox.Name = "downloadClipsBookmarksCbox";
downloadClipsBookmarksCbox.Size = new System.Drawing.Size(248, 19); downloadClipsBookmarksCbox.Size = new System.Drawing.Size(248, 19);
downloadClipsBookmarksCbox.TabIndex = 5; downloadClipsBookmarksCbox.TabIndex = 5;
@ -883,7 +913,7 @@
audiobookFixupsGb.Controls.Add(splitFilesByChapterCbox); audiobookFixupsGb.Controls.Add(splitFilesByChapterCbox);
audiobookFixupsGb.Controls.Add(stripUnabridgedCbox); audiobookFixupsGb.Controls.Add(stripUnabridgedCbox);
audiobookFixupsGb.Controls.Add(stripAudibleBrandingCbox); audiobookFixupsGb.Controls.Add(stripAudibleBrandingCbox);
audiobookFixupsGb.Location = new System.Drawing.Point(6, 229); audiobookFixupsGb.Location = new System.Drawing.Point(6, 254);
audiobookFixupsGb.Name = "audiobookFixupsGb"; audiobookFixupsGb.Name = "audiobookFixupsGb";
audiobookFixupsGb.Size = new System.Drawing.Size(416, 114); audiobookFixupsGb.Size = new System.Drawing.Size(416, 114);
audiobookFixupsGb.TabIndex = 19; audiobookFixupsGb.TabIndex = 19;
@ -1324,7 +1354,7 @@
// mergeOpeningEndCreditsCbox // mergeOpeningEndCreditsCbox
// //
mergeOpeningEndCreditsCbox.AutoSize = true; mergeOpeningEndCreditsCbox.AutoSize = true;
mergeOpeningEndCreditsCbox.Location = new System.Drawing.Point(19, 157); mergeOpeningEndCreditsCbox.Location = new System.Drawing.Point(19, 182);
mergeOpeningEndCreditsCbox.Name = "mergeOpeningEndCreditsCbox"; mergeOpeningEndCreditsCbox.Name = "mergeOpeningEndCreditsCbox";
mergeOpeningEndCreditsCbox.Size = new System.Drawing.Size(198, 19); mergeOpeningEndCreditsCbox.Size = new System.Drawing.Size(198, 19);
mergeOpeningEndCreditsCbox.TabIndex = 9; mergeOpeningEndCreditsCbox.TabIndex = 9;
@ -1334,7 +1364,7 @@
// retainAaxFileCbox // retainAaxFileCbox
// //
retainAaxFileCbox.AutoSize = true; retainAaxFileCbox.AutoSize = true;
retainAaxFileCbox.Location = new System.Drawing.Point(19, 133); retainAaxFileCbox.Location = new System.Drawing.Point(19, 158);
retainAaxFileCbox.Name = "retainAaxFileCbox"; retainAaxFileCbox.Name = "retainAaxFileCbox";
retainAaxFileCbox.Size = new System.Drawing.Size(131, 19); retainAaxFileCbox.Size = new System.Drawing.Size(131, 19);
retainAaxFileCbox.TabIndex = 8; retainAaxFileCbox.TabIndex = 8;
@ -1347,7 +1377,7 @@
downloadCoverArtCbox.AutoSize = true; downloadCoverArtCbox.AutoSize = true;
downloadCoverArtCbox.Checked = true; downloadCoverArtCbox.Checked = true;
downloadCoverArtCbox.CheckState = System.Windows.Forms.CheckState.Checked; 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.Name = "downloadCoverArtCbox";
downloadCoverArtCbox.Size = new System.Drawing.Size(162, 19); downloadCoverArtCbox.Size = new System.Drawing.Size(162, 19);
downloadCoverArtCbox.TabIndex = 4; downloadCoverArtCbox.TabIndex = 4;
@ -1360,7 +1390,7 @@
createCueSheetCbox.AutoSize = true; createCueSheetCbox.AutoSize = true;
createCueSheetCbox.Checked = true; createCueSheetCbox.Checked = true;
createCueSheetCbox.CheckState = System.Windows.Forms.CheckState.Checked; 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.Name = "createCueSheetCbox";
createCueSheetCbox.Size = new System.Drawing.Size(145, 19); createCueSheetCbox.Size = new System.Drawing.Size(145, 19);
createCueSheetCbox.TabIndex = 3; createCueSheetCbox.TabIndex = 3;
@ -1531,5 +1561,7 @@
private System.Windows.Forms.Button applyDisplaySettingsBtn; private System.Windows.Forms.Button applyDisplaySettingsBtn;
private System.Windows.Forms.ComboBox spatialAudioCodecCb; private System.Windows.Forms.ComboBox spatialAudioCodecCb;
private System.Windows.Forms.Label spatialCodecLbl; private System.Windows.Forms.Label spatialCodecLbl;
private System.Windows.Forms.CheckBox useWidevineCbox;
private System.Windows.Forms.CheckBox requestSpatialCbox;
} }
} }