Merge branch 'master' of https://github.com/rmcrackan/Libation
This commit is contained in:
commit
64a85b6aab
@ -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;
|
||||
|
||||
@ -61,9 +61,6 @@ namespace AaxDecrypter
|
||||
|
||||
#region Constants
|
||||
|
||||
//Size of each range request. Android app uses 64MB chunks.
|
||||
private const int RANGE_REQUEST_SZ = 64 * 1024 * 1024;
|
||||
|
||||
//Download memory buffer size
|
||||
private const int DOWNLOAD_BUFF_SZ = 8 * 1024;
|
||||
|
||||
@ -161,7 +158,7 @@ namespace AaxDecrypter
|
||||
|
||||
//Initiate connection with the first request block and
|
||||
//get the total content length before returning.
|
||||
using var client = new HttpClient();
|
||||
var client = new HttpClient();
|
||||
var response = await RequestNextByteRangeAsync(client);
|
||||
|
||||
if (ContentLength != 0 && ContentLength != response.FileSize)
|
||||
@ -170,38 +167,59 @@ namespace AaxDecrypter
|
||||
ContentLength = response.FileSize;
|
||||
|
||||
_downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
|
||||
//Hand off the open request to the downloader to download and write data to file.
|
||||
DownloadTask = Task.Run(() => DownloadLoopInternal(response), _cancellationSource.Token);
|
||||
//Hand off the client and the open request to the downloader to download and write data to file.
|
||||
DownloadTask = Task.Run(() => DownloadLoopInternal(client , response), _cancellationSource.Token);
|
||||
}
|
||||
|
||||
private async Task DownloadLoopInternal(BlockResponse initialResponse)
|
||||
private async Task DownloadLoopInternal(HttpClient client, BlockResponse blockResponse)
|
||||
{
|
||||
await DownloadToFile(initialResponse);
|
||||
initialResponse.Dispose();
|
||||
|
||||
try
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
long startPosition = WritePosition;
|
||||
|
||||
while (WritePosition < ContentLength && !IsCancelled)
|
||||
{
|
||||
using var response = await RequestNextByteRangeAsync(client);
|
||||
await DownloadToFile(response);
|
||||
try
|
||||
{
|
||||
await DownloadToFile(blockResponse);
|
||||
}
|
||||
catch (HttpIOException e)
|
||||
when (e.HttpRequestError is HttpRequestError.ResponseEnded
|
||||
&& WritePosition != startPosition
|
||||
&& WritePosition < ContentLength && !IsCancelled)
|
||||
{
|
||||
Serilog.Log.Logger.Debug($"The download connection ended before the file completed downloading all 0x{ContentLength:X10} bytes");
|
||||
|
||||
//the download made *some* progress since the last attempt.
|
||||
//Try again to complete the download from where it left off.
|
||||
//Make sure to rewind file to last flush position.
|
||||
_writeFile.Position = startPosition = WritePosition;
|
||||
blockResponse.Dispose();
|
||||
blockResponse = await RequestNextByteRangeAsync(client);
|
||||
|
||||
Serilog.Log.Logger.Debug($"Resuming the file download starting at position 0x{WritePosition:X10}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeFile.Close();
|
||||
_writeFile.Dispose();
|
||||
blockResponse.Dispose();
|
||||
client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<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 +244,7 @@ namespace AaxDecrypter
|
||||
private async Task DownloadToFile(BlockResponse block)
|
||||
{
|
||||
var endPosition = WritePosition + block.BlockSize;
|
||||
var networkStream = await block.Response.Content.ReadAsStreamAsync(_cancellationSource.Token);
|
||||
using var networkStream = await block.Response.Content.ReadAsStreamAsync(_cancellationSource.Token);
|
||||
|
||||
var downloadPosition = WritePosition;
|
||||
var nextFlush = downloadPosition + DATA_FLUSH_SZ;
|
||||
@ -286,7 +304,6 @@ namespace AaxDecrypter
|
||||
}
|
||||
finally
|
||||
{
|
||||
networkStream.Close();
|
||||
_downloadedPiece.Set();
|
||||
OnUpdate();
|
||||
}
|
||||
|
||||
@ -104,9 +104,6 @@ namespace ApplicationServices
|
||||
[Name("Content Type")]
|
||||
public string ContentType { get; set; }
|
||||
|
||||
[Name("Audio Format")]
|
||||
public string AudioFormat { get; set; }
|
||||
|
||||
[Name("Language")]
|
||||
public string Language { get; set; }
|
||||
|
||||
@ -152,7 +149,6 @@ namespace ApplicationServices
|
||||
BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(),
|
||||
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(),
|
||||
ContentType = a.Book.ContentType.ToString(),
|
||||
AudioFormat = a.Book.AudioFormat.ToString(),
|
||||
Language = a.Book.Language,
|
||||
LastDownloaded = a.Book.UserDefinedItem.LastDownloaded,
|
||||
LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "",
|
||||
@ -228,7 +224,6 @@ namespace ApplicationServices
|
||||
nameof(ExportDto.BookStatus),
|
||||
nameof(ExportDto.PdfStatus),
|
||||
nameof(ExportDto.ContentType),
|
||||
nameof(ExportDto.AudioFormat),
|
||||
nameof(ExportDto.Language),
|
||||
nameof(ExportDto.LastDownloaded),
|
||||
nameof(ExportDto.LastDownloadedVersion),
|
||||
@ -299,7 +294,6 @@ namespace ApplicationServices
|
||||
row.CreateCell(col++).SetCellValue(dto.BookStatus);
|
||||
row.CreateCell(col++).SetCellValue(dto.PdfStatus);
|
||||
row.CreateCell(col++).SetCellValue(dto.ContentType);
|
||||
row.CreateCell(col++).SetCellValue(dto.AudioFormat);
|
||||
row.CreateCell(col++).SetCellValue(dto.Language);
|
||||
|
||||
if (dto.LastDownloaded.HasValue)
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -39,8 +39,8 @@ namespace FileLiberator
|
||||
/// Path: in progress directory.
|
||||
/// File name: final file name.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// PDF: audio file does not exist
|
||||
@ -48,6 +48,12 @@ namespace FileLiberator
|
||||
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string 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>
|
||||
/// PDF: audio file already exists
|
||||
/// </summary>
|
||||
|
||||
@ -47,13 +47,18 @@ namespace FileLiberator
|
||||
if (libraryBook.Book.Audio_Exists())
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
|
||||
downloadValidation(libraryBook);
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var config = Configuration.Instance;
|
||||
using var downloadOptions = await DownloadOptions.InitiateDownloadAsync(api, config, libraryBook);
|
||||
|
||||
bool success = false;
|
||||
try
|
||||
{
|
||||
FilePathCache.Inserted += FilePathCache_Inserted;
|
||||
FilePathCache.Removed += FilePathCache_Removed;
|
||||
|
||||
success = await downloadAudiobookAsync(libraryBook);
|
||||
success = await downloadAudiobookAsync(api, config, downloadOptions);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -78,12 +83,12 @@ namespace FileLiberator
|
||||
var finalStorageDir = getDestinationDirectory(libraryBook);
|
||||
|
||||
var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries));
|
||||
Task[] finalTasks = new[]
|
||||
{
|
||||
Task.Run(() => downloadCoverArt(libraryBook)),
|
||||
Task[] finalTasks =
|
||||
[
|
||||
Task.Run(() => downloadCoverArt(downloadOptions)),
|
||||
moveFilesTask,
|
||||
Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir))
|
||||
};
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
@ -116,16 +121,9 @@ namespace FileLiberator
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> downloadAudiobookAsync(LibraryBook libraryBook)
|
||||
private async Task<bool> downloadAudiobookAsync(AudibleApi.Api api, Configuration config, DownloadOptions dlOptions)
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
downloadValidation(libraryBook);
|
||||
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
|
||||
using var dlOptions = await DownloadOptions.InitiateDownloadAsync(api, libraryBook, config);
|
||||
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower());
|
||||
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(dlOptions.LibraryBookDto, dlOptions.OutputFormat.ToString().ToLower());
|
||||
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
|
||||
|
||||
if (dlOptions.DrmType is not DrmType.Adrm and not DrmType.Widevine)
|
||||
@ -149,7 +147,7 @@ namespace FileLiberator
|
||||
abDownloader.RetrievedAuthors += OnAuthorsDiscovered;
|
||||
abDownloader.RetrievedNarrators += OnNarratorsDiscovered;
|
||||
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
||||
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
|
||||
abDownloader.FileCreated += (_, path) => OnFileCreated(dlOptions.LibraryBook, path);
|
||||
|
||||
// REAL WORK DONE HERE
|
||||
var success = await abDownloader.RunAsync();
|
||||
@ -158,12 +156,12 @@ namespace FileLiberator
|
||||
{
|
||||
var metadataFile = LibationFileManager.Templates.Templates.File.GetFilename(dlOptions.LibraryBookDto, Path.GetDirectoryName(outFileName), ".metadata.json");
|
||||
|
||||
var item = await api.GetCatalogProductAsync(libraryBook.Book.AudibleProductId, AudibleApi.CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
var item = await api.GetCatalogProductAsync(dlOptions.LibraryBook.Book.AudibleProductId, AudibleApi.CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
item.SourceJson.Add(nameof(ContentMetadata.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(dlOptions.ContentMetadata.ChapterInfo));
|
||||
item.SourceJson.Add(nameof(ContentMetadata.ContentReference), Newtonsoft.Json.Linq.JObject.FromObject(dlOptions.ContentMetadata.ContentReference));
|
||||
|
||||
File.WriteAllText(metadataFile, item.SourceJson.ToString());
|
||||
OnFileCreated(libraryBook, metadataFile);
|
||||
OnFileCreated(dlOptions.LibraryBook, metadataFile);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
@ -173,7 +171,7 @@ namespace FileLiberator
|
||||
if (sender is not AaxcDownloadConvertBase converter || converter.DownloadOptions is not DownloadOptions options)
|
||||
return;
|
||||
|
||||
tags.Title ??= options.LibraryBookDto.TitleWithSubtitle;
|
||||
tags.Title ??= options.LibraryBookDto.TitleWithSubtitle;
|
||||
tags.Album ??= tags.Title;
|
||||
tags.Artist ??= string.Join("; ", options.LibraryBook.Book.Authors.Select(a => a.Name));
|
||||
tags.AlbumArtists ??= tags.Artist;
|
||||
@ -280,7 +278,7 @@ namespace FileLiberator
|
||||
private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable<FilePathCache.CacheEntry> entries)
|
||||
=> entries.FirstOrDefault(f => f.FileType == FileType.Audio);
|
||||
|
||||
private static void downloadCoverArt(LibraryBook libraryBook)
|
||||
private static void downloadCoverArt(DownloadOptions options)
|
||||
{
|
||||
if (!Configuration.Instance.DownloadCoverArt) return;
|
||||
|
||||
@ -288,24 +286,24 @@ namespace FileLiberator
|
||||
|
||||
try
|
||||
{
|
||||
var destinationDir = getDestinationDirectory(libraryBook);
|
||||
coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, ".jpg");
|
||||
var destinationDir = getDestinationDirectory(options.LibraryBook);
|
||||
coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(options.LibraryBookDto, ".jpg");
|
||||
coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath));
|
||||
|
||||
if (File.Exists(coverPath))
|
||||
FileUtility.SaferDelete(coverPath);
|
||||
|
||||
var picBytes = PictureStorage.GetPictureSynchronously(new(libraryBook.Book.PictureLarge ?? libraryBook.Book.PictureId, PictureSize.Native));
|
||||
var picBytes = PictureStorage.GetPictureSynchronously(new(options.LibraryBook.Book.PictureLarge ?? options.LibraryBook.Book.PictureId, PictureSize.Native));
|
||||
if (picBytes.Length > 0)
|
||||
{
|
||||
File.WriteAllBytes(coverPath, picBytes);
|
||||
SetFileTime(libraryBook, coverPath);
|
||||
SetFileTime(options.LibraryBook, coverPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Failure to download cover art should not be considered a failure to download the book
|
||||
Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {libraryBook.Book.AudibleProductId} to {coverPath} catalog product.");
|
||||
Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {options.LibraryBook.Book.AudibleProductId} to {coverPath} catalog product.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
/// <summary>
|
||||
/// Initiate an audiobook download from the audible api.
|
||||
/// </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 options = BuildDownloadOptions(libraryBook, config, license);
|
||||
@ -33,81 +34,78 @@ public partial class DownloadOptions
|
||||
|
||||
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;
|
||||
|
||||
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.");
|
||||
//We failed to get a widevine license, so fall back to AAX(C)
|
||||
return await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
|
||||
}
|
||||
|
||||
if (!contentLic.ContentMetadata.ContentReference.IsSpatial && contentLic.DrmType != DrmType.Adrm)
|
||||
{
|
||||
/*
|
||||
We got a widevine license and we have a Cdm, but we still need to decide if we WANT the file
|
||||
being delivered with widevine. This file is not "spatial", so it may be no better than the
|
||||
audio in the Adrm files. All else being equal, we prefer Adrm files because they have more
|
||||
build-in metadata and always AAC-LC, which is a codec playable by pretty much every device
|
||||
in existence.
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
contentLic = fallback;
|
||||
}
|
||||
}
|
||||
|
||||
if (contentLic.DrmType == DrmType.Widevine && cdm is not null)
|
||||
if (contentLic.DrmType == DrmType.Widevine)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
var mpdResponse = await client.GetAsync(contentLic.LicenseResponse);
|
||||
using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse);
|
||||
var dash = new MpegDash(mpdResponse.Content.ReadAsStream());
|
||||
|
||||
if (!dash.TryGetUri(new Uri(contentLic.LicenseResponse), out var contentUri))
|
||||
@ -124,7 +122,6 @@ public partial class DownloadOptions
|
||||
Key = Convert.ToHexStringLower(keys[0].Kid.ToByteArray()),
|
||||
Iv = Convert.ToHexStringLower(keys[0].Key)
|
||||
};
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -160,9 +157,6 @@ public partial class DownloadOptions
|
||||
: contentLic.DrmType is DrmType.Adrm && contentLic.Voucher?.Key.Length == 32 && contentLic.Voucher?.Iv.Length == 32 ? AAXClean.FileType.Aaxc
|
||||
: null;
|
||||
|
||||
//Set the requested AudioFormat for use in file naming templates
|
||||
libraryBook.Book.AudioFormat = AudioFormat.FromString(contentLic.ContentMetadata.ContentReference.ContentFormat);
|
||||
|
||||
var dlOptions = new DownloadOptions(config, libraryBook, contentLic.ContentMetadata.ContentUrl?.OfflineUrl)
|
||||
{
|
||||
AudibleKey = contentLic.Voucher?.Key,
|
||||
@ -176,6 +170,14 @@ public partial class DownloadOptions
|
||||
RuntimeLength = TimeSpan.FromMilliseconds(contentLic.ContentMetadata.ChapterInfo.RuntimeLengthMs),
|
||||
};
|
||||
|
||||
dlOptions.LibraryBookDto.Codec = contentLic.ContentMetadata.ContentReference.Codec;
|
||||
if (TryGetAudioInfo(contentLic.ContentMetadata.ContentUrl, out int? bitrate, out int? sampleRate, out int? channels))
|
||||
{
|
||||
dlOptions.LibraryBookDto.BitRate = bitrate;
|
||||
dlOptions.LibraryBookDto.SampleRate = sampleRate;
|
||||
dlOptions.LibraryBookDto.Channels = channels;
|
||||
}
|
||||
|
||||
var titleConcat = config.CombineNestedChapterTitles ? ": " : null;
|
||||
var chapters
|
||||
= flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters, titleConcat)
|
||||
@ -202,6 +204,43 @@ public partial class DownloadOptions
|
||||
return dlOptions;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
LameConfig lameConfig = new()
|
||||
@ -359,4 +398,9 @@ public partial class DownloadOptions
|
||||
|
||||
static double RelativePercentDifference(long num1, long num2)
|
||||
=> Math.Abs(num1 - num2) / (double)(num1 + num2);
|
||||
|
||||
[GeneratedRegex(@".+_(\d+)_(\d+)-(\w+).mp4", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
|
||||
private static partial Regex WidevineAudioProperties();
|
||||
[GeneratedRegex(@".+_lc_(\d+)_(\d+)_(\d+).aax", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
|
||||
private static partial Regex AdrmAudioProperties();
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@ -43,14 +43,40 @@
|
||||
<controls:WheelComboBox
|
||||
Margin="5,0,0,0"
|
||||
Grid.Column="1"
|
||||
SelectionChanged="Quality_SelectionChanged"
|
||||
ItemsSource="{CompiledBinding DownloadQualities}"
|
||||
SelectedItem="{CompiledBinding FileDownloadQuality}"/>
|
||||
</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}">
|
||||
<Grid.IsEnabled>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<MultiBinding.Bindings>
|
||||
<CompiledBinding Path="UseWidevine"/>
|
||||
<CompiledBinding Path="RequestSpatial"/>
|
||||
</MultiBinding.Bindings>
|
||||
</MultiBinding>
|
||||
</Grid.IsEnabled>
|
||||
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
@ -63,6 +89,11 @@
|
||||
SelectedItem="{CompiledBinding SpatialAudioCodec}"/>
|
||||
</Grid>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding CreateCueSheet, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding CreateCueSheetText}" />
|
||||
</CheckBox>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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<EnumDisplay<Configuration.DownloadQuality>> DownloadQualities { get; } = new([
|
||||
new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.Normal),
|
||||
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([
|
||||
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 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<Configuration.DownloadQuality>? _fileDownloadQuality;
|
||||
public EnumDisplay<Configuration.DownloadQuality> 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<Configuration.DownloadQuality> FileDownloadQuality { get; set; }
|
||||
public EnumDisplay<Configuration.SpatialCodec> SpatialAudioCodec { get; set; }
|
||||
public Configuration.ClipBookmarkFormat ClipBookmarkFormat { get; set; }
|
||||
public bool MergeOpeningAndEndCredits { get; set; }
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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); }
|
||||
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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 <first series[{#}]>");
|
||||
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");
|
||||
|
||||
@ -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<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()
|
||||
{
|
||||
@ -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; } = "<title short> [<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))
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -23,6 +23,8 @@ namespace LibationWinForms.Dialogs
|
||||
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
|
||||
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
|
||||
this.moveMoovAtomCbox.Text = desc(nameof(config.MoveMoovToBeginning));
|
||||
this.useWidevineCbox.Text = desc(nameof(config.UseWidevine));
|
||||
this.requestSpatialCbox.Text = desc(nameof(config.RequestSpatial));
|
||||
this.spatialCodecLbl.Text = desc(nameof(config.SpatialAudioCodec));
|
||||
|
||||
toolTip.SetToolTip(combineNestedChapterTitlesCbox, Configuration.GetHelpText(nameof(config.CombineNestedChapterTitles)));
|
||||
@ -34,6 +36,8 @@ namespace LibationWinForms.Dialogs
|
||||
toolTip.SetToolTip(mergeOpeningEndCreditsCbox, Configuration.GetHelpText(nameof(config.MergeOpeningAndEndCredits)));
|
||||
toolTip.SetToolTip(retainAaxFileCbox, Configuration.GetHelpText(nameof(config.RetainAaxFile)));
|
||||
toolTip.SetToolTip(stripAudibleBrandingCbox, Configuration.GetHelpText(nameof(config.StripAudibleBrandAudio)));
|
||||
toolTip.SetToolTip(useWidevineCbox, Configuration.GetHelpText(nameof(config.UseWidevine)));
|
||||
toolTip.SetToolTip(requestSpatialCbox, Configuration.GetHelpText(nameof(config.RequestSpatial)));
|
||||
toolTip.SetToolTip(spatialCodecLbl, Configuration.GetHelpText(nameof(config.SpatialAudioCodec)));
|
||||
toolTip.SetToolTip(spatialAudioCodecCb, Configuration.GetHelpText(nameof(config.SpatialAudioCodec)));
|
||||
|
||||
@ -41,7 +45,6 @@ namespace LibationWinForms.Dialogs
|
||||
[
|
||||
new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.Normal),
|
||||
new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.High),
|
||||
new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.Spatial, "Spatial (if available)"),
|
||||
]);
|
||||
|
||||
spatialAudioCodecCb.Items.AddRange(
|
||||
@ -76,6 +79,8 @@ namespace LibationWinForms.Dialogs
|
||||
downloadClipsBookmarksCbox.Checked = config.DownloadClipsBookmarks;
|
||||
fileDownloadQualityCb.SelectedItem = config.FileDownloadQuality;
|
||||
spatialAudioCodecCb.SelectedItem = config.SpatialAudioCodec;
|
||||
useWidevineCbox.Checked = config.UseWidevine;
|
||||
requestSpatialCbox.Checked = config.RequestSpatial;
|
||||
|
||||
clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat;
|
||||
retainAaxFileCbox.Checked = config.RetainAaxFile;
|
||||
@ -118,6 +123,8 @@ namespace LibationWinForms.Dialogs
|
||||
config.DownloadCoverArt = downloadCoverArtCbox.Checked;
|
||||
config.DownloadClipsBookmarks = downloadClipsBookmarksCbox.Checked;
|
||||
config.FileDownloadQuality = ((EnumDisplay<Configuration.DownloadQuality>)fileDownloadQualityCb.SelectedItem).Value;
|
||||
config.UseWidevine = useWidevineCbox.Checked;
|
||||
config.RequestSpatial = requestSpatialCbox.Checked;
|
||||
config.SpatialAudioCodec = ((EnumDisplay<Configuration.SpatialCodec>)spatialAudioCodecCb.SelectedItem).Value;
|
||||
config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem;
|
||||
config.RetainAaxFile = retainAaxFileCbox.Checked;
|
||||
@ -140,7 +147,6 @@ namespace LibationWinForms.Dialogs
|
||||
config.ChapterTitleTemplate = chapterTitleTemplateTb.Text;
|
||||
}
|
||||
|
||||
|
||||
private void downloadClipsBookmarksCbox_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
clipsBookmarksFormatCb.Enabled = downloadClipsBookmarksCbox.Checked;
|
||||
@ -190,27 +196,28 @@ namespace LibationWinForms.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
private void fileDownloadQualityCb_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
var selectedSpatial = fileDownloadQualityCb.SelectedItem.Equals(Configuration.DownloadQuality.Spatial);
|
||||
|
||||
if (selectedSpatial)
|
||||
|
||||
private void useWidevineCbox_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (useWidevineCbox.Checked)
|
||||
{
|
||||
using var accounts = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
|
||||
if (!accounts.AccountsSettings.Accounts.Any(a => a.IdentityTokens.DeviceType == AudibleApi.Resources.DeviceType))
|
||||
{
|
||||
MessageBox.Show(this,
|
||||
"Your must remove account(s) from Libation and then re-add them to enable spatial audiobook downloads.",
|
||||
"Spatial Audio Unavailable",
|
||||
"Your must remove account(s) from Libation and then re-add them to enable widwvine content.",
|
||||
"Widevine Content Unavailable",
|
||||
MessageBoxButtons.OK);
|
||||
|
||||
fileDownloadQualityCb.SelectedItem = Configuration.DownloadQuality.High;
|
||||
useWidevineCbox.Checked = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
spatialCodecLbl.Enabled = spatialAudioCodecCb.Enabled = selectedSpatial;
|
||||
requestSpatialCbox.Enabled = useWidevineCbox.Checked;
|
||||
spatialCodecLbl.Enabled = spatialAudioCodecCb.Enabled = useWidevineCbox.Checked && requestSpatialCbox.Checked;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user