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 Dinah.Core; 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 @@ namespace LibationFileManager private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage(); - public static bool AaxcExists(string productId) => AAXC.Exists(productId); + public static bool AaxcExists(string productId) => AAXC.Exists(productId); - public static AudioFileStorage Audio { get; } = new AudioFileStorage(); + 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 + 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; } + #region instance + private FileType FileType { get; } + private string regexTemplate { get; } - protected AudibleFileStorage(FileType fileType) - { - FileType = fileType; + protected AudibleFileStorage(FileType fileType) + { + FileType = fileType; - var extAggr = FileTypes.GetExtensions(FileType).Aggregate((a, b) => $"{a}|{b}"); - regexTemplate = $@"{{0}}.*?\.({extAggr})$"; - } + 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; + 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); + // secondary lookup attempt + var firstOrNull = GetFilePathCustom(productId); + if (firstOrNull is not null) + FilePathCache.Insert(productId, firstOrNull); - return firstOrNull; - } + return firstOrNull; + } - public List GetPaths(string productId) - => GetFilePathsCustom(productId); + 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 - } + 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) { } + internal class AaxcFileStorage : AudibleFileStorage + { + internal AaxcFileStorage() : base(FileType.AAXC) { } - protected override LongPath? GetFilePathCustom(string productId) - => GetFilePathsCustom(productId).FirstOrDefault(); + 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(); - } + 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 bool Exists(string productId) => GetFilePath(productId) is not null; + } - public class AudioFileStorage : AudibleFileStorage - { - internal AudioFileStorage() : base(FileType.Audio) - => BookDirectoryFiles ??= newBookDirectoryFiles(); + 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 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 @@ namespace LibationFileManager //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(); - } + .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 void Refresh() + { + if (BookDirectoryFiles is null) + lock (bookDirectoryFilesLocker) + BookDirectoryFiles = newBookDirectoryFiles(); + else + BookDirectoryFiles?.RefreshFiles(); + } - public LongPath? GetPath(string productId) => GetFilePath(productId); + 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 (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); + 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 @@ namespace LibationFileManager 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; + } } } }