From 92d283187df44243c09662da0d3f44e373d42999 Mon Sep 17 00:00:00 2001 From: MBucari Date: Sun, 20 Aug 2023 11:38:43 -0600 Subject: [PATCH] Add support for locating mp3 audiobooks --- .../LibationFileManager/AudibleFileStorage.cs | 264 ++++++++++-------- 1 file changed, 146 insertions(+), 118 deletions(-) diff --git a/Source/LibationFileManager/AudibleFileStorage.cs b/Source/LibationFileManager/AudibleFileStorage.cs index 8ca7a356..0a89c8d9 100644 --- a/Source/LibationFileManager/AudibleFileStorage.cs +++ b/Source/LibationFileManager/AudibleFileStorage.cs @@ -8,20 +8,21 @@ using System.Threading.Tasks; using System.Threading; using FileManager; +using AaxDecrypter; #nullable enable namespace LibationFileManager { - public abstract class AudibleFileStorage - { - protected abstract LongPath? GetFilePathCustom(string productId); - protected abstract List GetFilePathsCustom(string productId); + public abstract class AudibleFileStorage + { + protected abstract LongPath? GetFilePathCustom(string productId); + protected abstract List GetFilePathsCustom(string productId); - #region static - public static LongPath DownloadsInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName; - public static LongPath DecryptInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName; + #region static + public static LongPath DownloadsInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName; + public static LongPath DecryptInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName; - static AudibleFileStorage() + static AudibleFileStorage() { //Clean up any partially-decrypted files from previous Libation instances. //Do no clean DownloadsInProgressDirectory because those files are resumable @@ -31,103 +32,103 @@ static AudibleFileStorage() private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage(); - public static bool AaxcExists(string productId) => AAXC.Exists(productId); - - public static AudioFileStorage Audio { get; } = new AudioFileStorage(); - - public static LongPath BooksDirectory - { - get - { - if (string.IsNullOrWhiteSpace(Configuration.Instance.Books)) - Configuration.Instance.Books = Path.Combine(Configuration.UserProfile, "Books"); - return Directory.CreateDirectory(Configuration.Instance.Books).FullName; - } - } - #endregion - - #region instance - private FileType FileType { get; } - private string regexTemplate { get; } - - protected AudibleFileStorage(FileType fileType) - { - FileType = fileType; - - var extAggr = FileTypes.GetExtensions(FileType).Aggregate((a, b) => $"{a}|{b}"); - regexTemplate = $@"{{0}}.*?\.({extAggr})$"; - } - - protected LongPath? GetFilePath(string productId) - { - // primary lookup - var cachedFile = FilePathCache.GetFirstPath(productId, FileType); - if (cachedFile is not null && File.Exists(cachedFile)) - return cachedFile; - - // secondary lookup attempt - var firstOrNull = GetFilePathCustom(productId); - if (firstOrNull is not null) - FilePathCache.Insert(productId, firstOrNull); - - return firstOrNull; - } - - public List GetPaths(string productId) - => GetFilePathsCustom(productId); - - protected Regex GetBookSearchRegex(string productId) - { - var pattern = string.Format(regexTemplate, productId); - return new Regex(pattern, RegexOptions.IgnoreCase); - } - #endregion - } - - internal class AaxcFileStorage : AudibleFileStorage - { - internal AaxcFileStorage() : base(FileType.AAXC) { } - - protected override LongPath? GetFilePathCustom(string productId) - => GetFilePathsCustom(productId).FirstOrDefault(); - - protected override List GetFilePathsCustom(string productId) - { - var regex = GetBookSearchRegex(productId); - return FileUtility - .SaferEnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories) - .Where(s => regex.IsMatch(s)).ToList(); - } - - public bool Exists(string productId) => GetFilePath(productId) is not null; - } - - public class AudioFileStorage : AudibleFileStorage - { - internal AudioFileStorage() : base(FileType.Audio) - => BookDirectoryFiles ??= newBookDirectoryFiles(); - - private static BackgroundFileSystem? BookDirectoryFiles { get; set; } - private static object bookDirectoryFilesLocker { get; } = new(); + public static bool AaxcExists(string productId) => AAXC.Exists(productId); + + public static AudioFileStorage Audio { get; } = new AudioFileStorage(); + + public static LongPath BooksDirectory + { + get + { + if (string.IsNullOrWhiteSpace(Configuration.Instance.Books)) + Configuration.Instance.Books = Path.Combine(Configuration.UserProfile, "Books"); + return Directory.CreateDirectory(Configuration.Instance.Books).FullName; + } + } + #endregion + + #region instance + private FileType FileType { get; } + private string regexTemplate { get; } + + protected AudibleFileStorage(FileType fileType) + { + FileType = fileType; + + var extAggr = FileTypes.GetExtensions(FileType).Aggregate((a, b) => $"{a}|{b}"); + regexTemplate = $@"{{0}}.*?\.({extAggr})$"; + } + + protected LongPath? GetFilePath(string productId) + { + // primary lookup + var cachedFile = FilePathCache.GetFirstPath(productId, FileType); + if (cachedFile is not null && File.Exists(cachedFile)) + return cachedFile; + + // secondary lookup attempt + var firstOrNull = GetFilePathCustom(productId); + if (firstOrNull is not null) + FilePathCache.Insert(productId, firstOrNull); + + return firstOrNull; + } + + public List GetPaths(string productId) + => GetFilePathsCustom(productId); + + protected Regex GetBookSearchRegex(string productId) + { + var pattern = string.Format(regexTemplate, productId); + return new Regex(pattern, RegexOptions.IgnoreCase); + } + #endregion + } + + internal class AaxcFileStorage : AudibleFileStorage + { + internal AaxcFileStorage() : base(FileType.AAXC) { } + + protected override LongPath? GetFilePathCustom(string productId) + => GetFilePathsCustom(productId).FirstOrDefault(); + + protected override List GetFilePathsCustom(string productId) + { + var regex = GetBookSearchRegex(productId); + return FileUtility + .SaferEnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories) + .Where(s => regex.IsMatch(s)).ToList(); + } + + public bool Exists(string productId) => GetFilePath(productId) is not null; + } + + public class AudioFileStorage : AudibleFileStorage + { + internal AudioFileStorage() : base(FileType.Audio) + => BookDirectoryFiles ??= newBookDirectoryFiles(); + + private static BackgroundFileSystem? BookDirectoryFiles { get; set; } + private static object bookDirectoryFilesLocker { get; } = new(); private static EnumerationOptions enumerationOptions { get; } = new() { RecurseSubdirectories = true, IgnoreInaccessible = true, - MatchCasing = MatchCasing.CaseInsensitive + AttributesToSkip = FileAttributes.Hidden, }; protected override LongPath? GetFilePathCustom(string productId) - => GetFilePathsCustom(productId).FirstOrDefault(); + => GetFilePathsCustom(productId).FirstOrDefault(); - private static BackgroundFileSystem newBookDirectoryFiles() - => new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); + private static BackgroundFileSystem newBookDirectoryFiles() + => new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); protected override List GetFilePathsCustom(string productId) - { - // If user changed the BooksDirectory: reinitialize - lock (bookDirectoryFilesLocker) - if (BooksDirectory != BookDirectoryFiles?.RootDirectory) - BookDirectoryFiles = newBookDirectoryFiles(); + { + // If user changed the BooksDirectory: reinitialize + lock (bookDirectoryFilesLocker) + if (BooksDirectory != BookDirectoryFiles?.RootDirectory) + BookDirectoryFiles = newBookDirectoryFiles(); var regex = GetBookSearchRegex(productId); @@ -135,44 +136,62 @@ protected override List GetFilePathsCustom(string productId) //using both the file system and the file path cache return FilePathCache - .GetFiles(productId) - .Where(c => c.fileType == FileType.Audio && File.Exists(c.path)) - .Select(c => c.path) - .Union(BookDirectoryFiles.FindFiles(regex)) - .ToList(); - } - - public void Refresh() - { - if (BookDirectoryFiles is null) - lock (bookDirectoryFilesLocker) - BookDirectoryFiles = newBookDirectoryFiles(); - else - BookDirectoryFiles?.RefreshFiles(); - } - - public LongPath? GetPath(string productId) => GetFilePath(productId); + .GetFiles(productId) + .Where(c => c.fileType == FileType.Audio && File.Exists(c.path)) + .Select(c => c.path) + .Union(BookDirectoryFiles.FindFiles(regex)) + .ToList(); + } + + public void Refresh() + { + if (BookDirectoryFiles is null) + lock (bookDirectoryFilesLocker) + BookDirectoryFiles = newBookDirectoryFiles(); + else + BookDirectoryFiles?.RefreshFiles(); + } + + public LongPath? GetPath(string productId) => GetFilePath(productId); public static async IAsyncEnumerable FindAudiobooksAsync(LongPath searchDirectory, [EnumeratorCancellation] CancellationToken cancellationToken) { ArgumentValidator.EnsureNotNull(searchDirectory, nameof(searchDirectory)); - foreach (LongPath path in Directory.EnumerateFiles(searchDirectory, "*.M4B", enumerationOptions)) + foreach (LongPath path in Directory.EnumerateFiles(searchDirectory, "*.*", enumerationOptions)) { if (cancellationToken.IsCancellationRequested) yield break; + if (getFormatByExtension(path) is not OutputFormat format) + continue; + FilePathCache.CacheEntry? audioFile = default; try { using var fileStream = File.OpenRead(path); - var mp4File = await Task.Run(() => new AAXClean.Mp4File(fileStream), cancellationToken); - - if (mp4File?.AppleTags?.Asin is not null) - audioFile = new FilePathCache.CacheEntry(mp4File.AppleTags.Asin, FileType.Audio, path); - + if (format is OutputFormat.M4b) + { + var mp4File = await Task.Run(() => new AAXClean.Mp4File(fileStream), cancellationToken); + + if (mp4File?.AppleTags?.Asin is not null) + audioFile = new FilePathCache.CacheEntry(mp4File.AppleTags.Asin, FileType.Audio, path); + } + else + { + var id3 = NAudio.Lame.ID3.Id3Tag.Create(fileStream); + + var asin + = id3?.Children + .OfType() + .FirstOrDefault(f => f.FieldName == "AUDIBLE_ASIN") + ?.FieldValue; + + if (!string.IsNullOrWhiteSpace(asin)) + audioFile = new FilePathCache.CacheEntry(asin, FileType.Audio, path); + } } catch (Exception ex) { @@ -186,6 +205,15 @@ public void Refresh() if (audioFile is not null) yield return audioFile; } + + static OutputFormat? getFormatByExtension(string path) + { + var ext = Path.GetExtension(path).ToLower(); + + return ext == ".mp3" ? OutputFormat.Mp3 + : ext == ".m4b" ? OutputFormat.M4b + : null; + } } } }