Add support for locating mp3 audiobooks

This commit is contained in:
MBucari 2023-08-20 11:38:43 -06:00
parent 51b8cfe71f
commit 92d283187d

View File

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