diff --git a/Source/FileLiberator/AudioFileStorageExt.cs b/Source/FileLiberator/AudioFileStorageExt.cs
index ff47fd59..c0ce9bd0 100644
--- a/Source/FileLiberator/AudioFileStorageExt.cs
+++ b/Source/FileLiberator/AudioFileStorageExt.cs
@@ -39,14 +39,20 @@ namespace FileLiberator
/// Path: in progress directory.
/// File name: final file name.
///
- public static string GetInProgressFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
- => Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.DecryptInProgressDirectory, extension, returnFirstExisting: true);
+ public static string GetInProgressFilename(this AudioFileStorage _, LibraryBookDto libraryBook, string extension)
+ => Templates.File.GetFilename(libraryBook, AudibleFileStorage.DecryptInProgressDirectory, extension, returnFirstExisting: true);
///
/// PDF: audio file does not exist
///
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
=> Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.BooksDirectory, extension);
+
+ ///
+ /// PDF: audio file does not exist
+ ///
+ public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBookDto dto, string extension)
+ => Templates.File.GetFilename(dto, AudibleFileStorage.BooksDirectory, extension);
///
/// PDF: audio file already exists
diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs
index b6de8021..0f92ce8c 100644
--- a/Source/FileLiberator/DownloadDecryptBook.cs
+++ b/Source/FileLiberator/DownloadDecryptBook.cs
@@ -47,13 +47,18 @@ namespace FileLiberator
if (libraryBook.Book.Audio_Exists())
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
+ downloadValidation(libraryBook);
+ var api = await libraryBook.GetApiAsync();
+ var config = Configuration.Instance;
+ using var downloadOptions = await DownloadOptions.InitiateDownloadAsync(api, config, libraryBook);
+
bool success = false;
try
{
FilePathCache.Inserted += FilePathCache_Inserted;
FilePathCache.Removed += FilePathCache_Removed;
- success = await downloadAudiobookAsync(libraryBook);
+ success = await downloadAudiobookAsync(api, config, downloadOptions);
}
finally
{
@@ -78,12 +83,12 @@ namespace FileLiberator
var finalStorageDir = getDestinationDirectory(libraryBook);
var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries));
- Task[] finalTasks = new[]
- {
- Task.Run(() => downloadCoverArt(libraryBook)),
+ Task[] finalTasks =
+ [
+ Task.Run(() => downloadCoverArt(downloadOptions)),
moveFilesTask,
Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir))
- };
+ ];
try
{
@@ -116,16 +121,11 @@ namespace FileLiberator
}
}
- private async Task downloadAudiobookAsync(LibraryBook libraryBook)
+
+
+ private async Task downloadAudiobookAsync(AudibleApi.Api api, Configuration config, DownloadOptions dlOptions)
{
- var config = Configuration.Instance;
-
- downloadValidation(libraryBook);
-
- var api = await libraryBook.GetApiAsync();
-
- using var dlOptions = await DownloadOptions.InitiateDownloadAsync(api, libraryBook, config);
- var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower());
+ var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(dlOptions.LibraryBookDto, dlOptions.OutputFormat.ToString().ToLower());
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
if (dlOptions.DrmType is not DrmType.Adrm and not DrmType.Widevine)
@@ -149,7 +149,7 @@ namespace FileLiberator
abDownloader.RetrievedAuthors += OnAuthorsDiscovered;
abDownloader.RetrievedNarrators += OnNarratorsDiscovered;
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
- abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
+ abDownloader.FileCreated += (_, path) => OnFileCreated(dlOptions.LibraryBook, path);
// REAL WORK DONE HERE
var success = await abDownloader.RunAsync();
@@ -158,12 +158,12 @@ namespace FileLiberator
{
var metadataFile = LibationFileManager.Templates.Templates.File.GetFilename(dlOptions.LibraryBookDto, Path.GetDirectoryName(outFileName), ".metadata.json");
- var item = await api.GetCatalogProductAsync(libraryBook.Book.AudibleProductId, AudibleApi.CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
+ var item = await api.GetCatalogProductAsync(dlOptions.LibraryBook.Book.AudibleProductId, AudibleApi.CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
item.SourceJson.Add(nameof(ContentMetadata.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(dlOptions.ContentMetadata.ChapterInfo));
item.SourceJson.Add(nameof(ContentMetadata.ContentReference), Newtonsoft.Json.Linq.JObject.FromObject(dlOptions.ContentMetadata.ContentReference));
File.WriteAllText(metadataFile, item.SourceJson.ToString());
- OnFileCreated(libraryBook, metadataFile);
+ OnFileCreated(dlOptions.LibraryBook, metadataFile);
}
return success;
}
@@ -173,7 +173,7 @@ namespace FileLiberator
if (sender is not AaxcDownloadConvertBase converter || converter.DownloadOptions is not DownloadOptions options)
return;
- tags.Title ??= options.LibraryBookDto.TitleWithSubtitle;
+ tags.Title ??= options.LibraryBookDto.TitleWithSubtitle;
tags.Album ??= tags.Title;
tags.Artist ??= string.Join("; ", options.LibraryBook.Book.Authors.Select(a => a.Name));
tags.AlbumArtists ??= tags.Artist;
@@ -280,7 +280,7 @@ namespace FileLiberator
private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable entries)
=> entries.FirstOrDefault(f => f.FileType == FileType.Audio);
- private static void downloadCoverArt(LibraryBook libraryBook)
+ private static void downloadCoverArt(DownloadOptions options)
{
if (!Configuration.Instance.DownloadCoverArt) return;
@@ -288,24 +288,24 @@ namespace FileLiberator
try
{
- var destinationDir = getDestinationDirectory(libraryBook);
- coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, ".jpg");
+ var destinationDir = getDestinationDirectory(options.LibraryBook);
+ coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(options.LibraryBookDto, ".jpg");
coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath));
if (File.Exists(coverPath))
FileUtility.SaferDelete(coverPath);
- var picBytes = PictureStorage.GetPictureSynchronously(new(libraryBook.Book.PictureLarge ?? libraryBook.Book.PictureId, PictureSize.Native));
+ var picBytes = PictureStorage.GetPictureSynchronously(new(options.LibraryBook.Book.PictureLarge ?? options.LibraryBook.Book.PictureId, PictureSize.Native));
if (picBytes.Length > 0)
{
File.WriteAllBytes(coverPath, picBytes);
- SetFileTime(libraryBook, coverPath);
+ SetFileTime(options.LibraryBook, coverPath);
}
}
catch (Exception ex)
{
//Failure to download cover art should not be considered a failure to download the book
- Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {libraryBook.Book.AudibleProductId} to {coverPath} catalog product.");
+ Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {options.LibraryBook.Book.AudibleProductId} to {coverPath} catalog product.");
}
}
}
diff --git a/Source/FileLiberator/DownloadOptions.Factory.cs b/Source/FileLiberator/DownloadOptions.Factory.cs
index cfccd55c..63db1f4f 100644
--- a/Source/FileLiberator/DownloadOptions.Factory.cs
+++ b/Source/FileLiberator/DownloadOptions.Factory.cs
@@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
#nullable enable
@@ -23,7 +24,7 @@ public partial class DownloadOptions
///
/// Initiate an audiobook download from the audible api.
///
- public static async Task InitiateDownloadAsync(Api api, LibraryBook libraryBook, Configuration config)
+ public static async Task InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook)
{
var license = await ChooseContent(api, libraryBook, config);
var options = BuildDownloadOptions(libraryBook, config, license);
@@ -172,6 +173,14 @@ public partial class DownloadOptions
RuntimeLength = TimeSpan.FromMilliseconds(contentLic.ContentMetadata.ChapterInfo.RuntimeLengthMs),
};
+ dlOptions.LibraryBookDto.Codec = contentLic.ContentMetadata.ContentReference.Codec;
+ if (TryGetAudioInfo(contentLic.ContentMetadata.ContentUrl, out int? bitrate, out int? sampleRate, out int? channels))
+ {
+ dlOptions.LibraryBookDto.BitRate = bitrate;
+ dlOptions.LibraryBookDto.SampleRate = sampleRate;
+ dlOptions.LibraryBookDto.Channels = channels;
+ }
+
var titleConcat = config.CombineNestedChapterTitles ? ": " : null;
var chapters
= flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters, titleConcat)
@@ -198,6 +207,43 @@ public partial class DownloadOptions
return dlOptions;
}
+ ///
+ /// The most reliable way to get these audio file properties is from the filename itself.
+ /// Using AAXClean to read the metadata works well for everything except AC-4 bitrate.
+ ///
+ private static bool TryGetAudioInfo(ContentUrl? contentUrl, out int? bitrate, out int? sampleRate, out int? channels)
+ {
+ bitrate = sampleRate = channels = null;
+
+ if (contentUrl?.OfflineUrl is not string url || !Uri.TryCreate(url, default, out var uri))
+ return false;
+
+ var file = Path.GetFileName(uri.LocalPath);
+
+ var match = AdrmAudioProperties().Match(file);
+ if (match.Success)
+ {
+ bitrate = int.Parse(match.Groups[1].Value);
+ sampleRate = int.Parse(match.Groups[2].Value);
+ channels = int.Parse(match.Groups[3].Value);
+ return true;
+ }
+ else if ((match = WidevineAudioProperties().Match(file)).Success)
+ {
+ bitrate = int.Parse(match.Groups[2].Value);
+ sampleRate = int.Parse(match.Groups[1].Value) * 1000;
+ channels = match.Groups[3].Value switch
+ {
+ "ec3" => 6,
+ "ac4" => 3,
+ _ => null
+ };
+ return true;
+ }
+
+ return false;
+ }
+
public static LameConfig GetLameOptions(Configuration config)
{
LameConfig lameConfig = new()
@@ -355,4 +401,9 @@ public partial class DownloadOptions
static double RelativePercentDifference(long num1, long num2)
=> Math.Abs(num1 - num2) / (double)(num1 + num2);
+
+ [GeneratedRegex(@".+_(\d+)_(\d+)-(\w+).mp4", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
+ private static partial Regex WidevineAudioProperties();
+ [GeneratedRegex(@".+_lc_(\d+)_(\d+)_(\d+).aax", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
+ private static partial Regex AdrmAudioProperties();
}
diff --git a/Source/FileLiberator/UtilityExtensions.cs b/Source/FileLiberator/UtilityExtensions.cs
index 6a920990..08f11a50 100644
--- a/Source/FileLiberator/UtilityExtensions.cs
+++ b/Source/FileLiberator/UtilityExtensions.cs
@@ -55,9 +55,6 @@ namespace FileLiberator
IsPodcastParent = libraryBook.Book.IsEpisodeParent(),
IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(),
- BitRate = libraryBook.Book.AudioFormat.Bitrate,
- SampleRate = libraryBook.Book.AudioFormat.SampleRate,
- Channels = libraryBook.Book.AudioFormat.Channels,
Language = libraryBook.Book.Language
};
}
diff --git a/Source/LibationFileManager/Templates/LibraryBookDto.cs b/Source/LibationFileManager/Templates/LibraryBookDto.cs
index be26c989..dc32fa9c 100644
--- a/Source/LibationFileManager/Templates/LibraryBookDto.cs
+++ b/Source/LibationFileManager/Templates/LibraryBookDto.cs
@@ -27,9 +27,10 @@ public class BookDto
public bool IsPodcastParent { get; set; }
public bool IsPodcast { get; set; }
- public int BitRate { get; set; }
- public int SampleRate { get; set; }
- public int Channels { get; set; }
+ public int? BitRate { get; set; }
+ public int? SampleRate { get; set; }
+ public int? Channels { get; set; }
+ public string? Codec { get; set; }
public DateTime FileDate { get; set; } = DateTime.Now;
public DateTime? DatePublished { get; set; }
public string? Language { get; set; }
diff --git a/Source/LibationFileManager/Templates/TemplateTags.cs b/Source/LibationFileManager/Templates/TemplateTags.cs
index d5bead6d..e8cdb1df 100644
--- a/Source/LibationFileManager/Templates/TemplateTags.cs
+++ b/Source/LibationFileManager/Templates/TemplateTags.cs
@@ -36,9 +36,10 @@ namespace LibationFileManager.Templates
public static TemplateTags Series { get; } = new TemplateTags("series", "All series to which the book belongs (if any)");
public static TemplateTags FirstSeries { get; } = new TemplateTags("first series", "First series");
public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series (alias for ");
- public static TemplateTags Bitrate { get; } = new TemplateTags("bitrate", "File's orig. bitrate");
- public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "File's orig. sample rate");
- public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels");
+ public static TemplateTags Bitrate { get; } = new TemplateTags("bitrate", "Audiobook's source bitrate");
+ public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "Audiobook's source sample rate");
+ public static TemplateTags Channels { get; } = new TemplateTags("channels", "Audiobook's source audio channel count");
+ public static TemplateTags Codec { get; } = new TemplateTags("codec", "Audiobook's source codec");
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
public static TemplateTags AccountNickname { get; } = new TemplateTags("account nickname", "Audible account nickname of this book");
public static TemplateTags Locale { get; } = new("locale", "Region/country");
diff --git a/Source/LibationFileManager/Templates/Templates.cs b/Source/LibationFileManager/Templates/Templates.cs
index c054459d..bfba1468 100644
--- a/Source/LibationFileManager/Templates/Templates.cs
+++ b/Source/LibationFileManager/Templates/Templates.cs
@@ -271,9 +271,6 @@ namespace LibationFileManager.Templates
{ TemplateTags.Language, lb => lb.Language },
//Don't allow formatting of LanguageShort
{ TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort },
- { TemplateTags.Bitrate, lb => (int?)(lb.IsPodcastParent ? null : lb.BitRate) },
- { TemplateTags.SampleRate, lb => (int?)(lb.IsPodcastParent ? null : lb.SampleRate) },
- { TemplateTags.Channels, lb => (int?)(lb.IsPodcastParent ? null : lb.Channels) },
{ TemplateTags.Account, lb => lb.Account },
{ TemplateTags.AccountNickname, lb => lb.AccountNickname },
{ TemplateTags.Locale, lb => lb.Locale },
@@ -281,7 +278,16 @@ namespace LibationFileManager.Templates
{ TemplateTags.DatePublished, lb => lb.DatePublished },
{ TemplateTags.DateAdded, lb => lb.DateAdded },
{ TemplateTags.FileDate, lb => lb.FileDate },
- };
+ };
+
+ private static readonly PropertyTagCollection audioFilePropertyTags =
+ new(caseSensative: true, StringFormatter, IntegerFormatter)
+ {
+ { TemplateTags.Bitrate, lb => lb.BitRate },
+ { TemplateTags.SampleRate, lb => lb.SampleRate },
+ { TemplateTags.Channels, lb => lb.Channels },
+ { TemplateTags.Codec, lb => lb.Codec },
+ };
private static readonly List chapterPropertyTags = new()
{
@@ -376,8 +382,7 @@ namespace LibationFileManager.Templates
public static string Name { get; } = "Folder Template";
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? "";
public static string DefaultTemplate { get; } = " []";
- public static IEnumerable TagCollections
- => new TagCollection[] { filePropertyTags, conditionalTags, folderConditionalTags };
+ public static IEnumerable TagCollections { get; } = [filePropertyTags, conditionalTags, folderConditionalTags];
public override IEnumerable 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; } = " []";
- public static IEnumerable TagCollections { get; } = new TagCollection[] { filePropertyTags, conditionalTags };
+ public static IEnumerable 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; } = " [] - - ";
- public static IEnumerable TagCollections { get; } = chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags);
+ public static IEnumerable TagCollections { get; }
+ = chapterPropertyTags.Append(filePropertyTags).Append(audioFilePropertyTags).Append(conditionalTags);
public override IEnumerable Warnings
=> NamingTemplate.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName))