Add support for locating mp3 audiobooks
This commit is contained in:
parent
51b8cfe71f
commit
92d283187d
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user