diff --git a/AaxDecrypter/AaxcDownloadConverter.cs b/AaxDecrypter/AaxcDownloadConverter.cs index 2cbc8580..26a6abff 100644 --- a/AaxDecrypter/AaxcDownloadConverter.cs +++ b/AaxDecrypter/AaxcDownloadConverter.cs @@ -87,6 +87,30 @@ namespace AaxDecrypter return success; } + /* +https://github.com/rmcrackan/Libation/pull/127#issuecomment-939088489 + +If the chapter truly is empty, that is, 0 audio frames in length, then yes it is ignored. +If the chapter is shorter than 3 seconds long but still has some audio frames, those frames are combined with the following chapter and not split into a new file. + +I also implemented file naming by chapter title. When 2 or more consecutive chapters are combined, the first of the combined chapter's title is used in the file name. For example, given an audiobook with the following chapters: + +00:00:00 - 00:00:02 | Part 1 +00:00:02 - 00:35:00 | Chapter 1 +00:35:02 - 01:02:00 | Chapter 2 +01:02:00 - 01:02:02 | Part 2 +01:02:02 - 01:41:00 | Chapter 3 +01:41:00 - 02:05:00 | Chapter 4 + +The book will be split into the following files: + +00:00:00 - 00:35:00 | Book - 01 - Part 1.m4b +00:35:00 - 01:02:00 | Book - 02 - Chapter 2.m4b +01:02:00 - 01:41:00 | Book - 03 - Part 2.m4b +01:41:00 - 02:05:00 | Book - 04 - Chapter 4.m4b + +That naming may not be desirable for everyone, but it's an easy change to instead use the last of the combined chapter's title in the file name. + */ private bool Step2_DownloadAudiobookAsMultipleFilesPerChapter() { var zeroProgress = Step2_Start(); diff --git a/AaxDecrypter/AudiobookDownloadBase.cs b/AaxDecrypter/AudiobookDownloadBase.cs index bd89d4ca..22a523ee 100644 --- a/AaxDecrypter/AudiobookDownloadBase.cs +++ b/AaxDecrypter/AudiobookDownloadBase.cs @@ -104,7 +104,9 @@ namespace AaxDecrypter // not a critical step. its failure should not prevent future steps from running try { - File.WriteAllText(PathLib.ReplaceExtension(OutputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadLicense.ChapterInfo)); + var path = PathLib.ReplaceExtension(OutputFileName, ".cue"); + File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadLicense.ChapterInfo)); + OnFileCreated(path); } catch (Exception ex) { diff --git a/AppScaffolding/AppScaffolding.csproj b/AppScaffolding/AppScaffolding.csproj index d8a29a10..1b965e50 100644 --- a/AppScaffolding/AppScaffolding.csproj +++ b/AppScaffolding/AppScaffolding.csproj @@ -3,7 +3,7 @@ net5.0 - 6.2.4.0 + 6.2.5.0 diff --git a/AppScaffolding/LibationScaffolding.cs b/AppScaffolding/LibationScaffolding.cs index 41eb62bd..d4b39386 100644 --- a/AppScaffolding/LibationScaffolding.cs +++ b/AppScaffolding/LibationScaffolding.cs @@ -211,11 +211,11 @@ namespace AppScaffolding config.InProgress, - DownloadsInProgressDir = AudibleFileStorage.DownloadsInProgress, - DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgress).Count(), + DownloadsInProgressDirectory = AudibleFileStorage.DownloadsInProgressDirectory, + DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgressDirectory).Count(), - DecryptInProgressDir = AudibleFileStorage.DecryptInProgress, - DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgress).Count(), + DecryptInProgressDirectory = AudibleFileStorage.DecryptInProgressDirectory, + DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(), }); } diff --git a/FileLiberator/ConvertToMp3.cs b/FileLiberator/ConvertToMp3.cs index abb1e6e2..b10dcd7b 100644 --- a/FileLiberator/ConvertToMp3.cs +++ b/FileLiberator/ConvertToMp3.cs @@ -55,7 +55,7 @@ namespace FileLiberator var mp3Path = Mp3FileName(m4bPath); FileExt.SafeMove(mp3File.Name, mp3Path); - OnFileCreated(libraryBook.Book.AudibleProductId, FileManager.FileType.Audio, mp3Path); + OnFileCreated(libraryBook.Book.AudibleProductId, mp3Path); var statusHandler = new StatusHandler(); diff --git a/FileLiberator/DownloadDecryptBook.cs b/FileLiberator/DownloadDecryptBook.cs index b794157b..0bf31799 100644 --- a/FileLiberator/DownloadDecryptBook.cs +++ b/FileLiberator/DownloadDecryptBook.cs @@ -16,8 +16,27 @@ namespace FileLiberator { private AudiobookDownloadBase abDownloader; + public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists; + + public override void Cancel() => abDownloader?.Cancel(); + public override async Task ProcessAsync(LibraryBook libraryBook) { + var entries = new List(); + // these only work so minimally b/c CacheEntry is a record. + // in case of parallel decrypts, only capture the ones for this book id. + // if user somehow starts multiple decrypts of the same book in parallel: on their own head be it + void FilePathCache_Inserted(object sender, FilePathCache.CacheEntry e) + { + if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId)) + entries.Add(e); + } + void FilePathCache_Removed(object sender, FilePathCache.CacheEntry e) + { + if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId)) + entries.Remove(e); + } + OnBegin(libraryBook); try @@ -25,15 +44,19 @@ namespace FileLiberator if (libraryBook.Book.Audio_Exists) return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; - var outputAudioFilename = await downloadAudiobookAsync(libraryBook); + FilePathCache.Inserted += FilePathCache_Inserted; + FilePathCache.Removed += FilePathCache_Removed; + + var success = await downloadAudiobookAsync(libraryBook); // decrypt failed - if (outputAudioFilename is null) + if (!success) return new StatusHandler { "Decrypt failed" }; // moves new files from temp dir to final dest - var movedAudioFile = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename); + var movedAudioFile = moveFilesToBooksDir(libraryBook.Book, entries); + // decrypt failed if (!movedAudioFile) return new StatusHandler { "Cannot find final audio file after decryption" }; @@ -43,17 +66,20 @@ namespace FileLiberator } finally { + FilePathCache.Inserted -= FilePathCache_Inserted; + FilePathCache.Removed -= FilePathCache_Removed; + OnCompleted(libraryBook); } } - private async Task downloadAudiobookAsync(LibraryBook libraryBook) + private async Task downloadAudiobookAsync(LibraryBook libraryBook) { OnStreamingBegin($"Begin decrypting {libraryBook}"); try { - validate(libraryBook); + downloadValidation(libraryBook); var api = await libraryBook.GetApiAsync(); var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId); @@ -82,9 +108,9 @@ namespace FileLiberator audiobookDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs)); } - var outFileName = Path.Combine(AudibleFileStorage.DecryptInProgress, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].{outputFormat.ToString().ToLower()}"); + var outFileName = Path.Combine(AudibleFileStorage.DecryptInProgressDirectory, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].{outputFormat.ToString().ToLower()}"); - var cacheDir = AudibleFileStorage.DownloadsInProgress; + var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory; abDownloader = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm ? new AaxcDownloadConverter(outFileName, cacheDir, audiobookDlLic, outputFormat, Configuration.Instance.SplitFilesByChapter) @@ -95,16 +121,12 @@ namespace FileLiberator abDownloader.RetrievedAuthors += (_, authors) => OnAuthorsDiscovered(authors); abDownloader.RetrievedNarrators += (_, narrators) => OnNarratorsDiscovered(narrators); abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt; - abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook.Book.AudibleProductId, FileType.Audio, path); + abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook.Book.AudibleProductId, path); // REAL WORK DONE HERE var success = await Task.Run(abDownloader.Run); - // decrypt failed - if (!success) - return null; - - return outFileName; + return success; } finally { @@ -112,81 +134,7 @@ namespace FileLiberator } } - private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e) - { - if (e is null && Configuration.Instance.AllowLibationFixup) - { - OnRequestCoverArt(abDownloader.SetCoverArt); - } - - if (e is not null) - { - OnCoverImageDiscovered(e); - } - } - - /// Move new files to 'Books' directory - /// True if audiobook file(s) were successfully created and can be located on disk. Else false. - private static bool moveFilesToBooksDir(Book book, string outputAudioFilename) - { - // create final directory. move each file into it. MOVE AUDIO FILE LAST - // new dir: safetitle_limit50char + " [" + productId + "]" - // TODO make this method handle multiple audio files or a single audio file. - var destinationDir = AudibleFileStorage.Audio.GetDestDir(book.Title, book.AudibleProductId); - Directory.CreateDirectory(destinationDir); - - var sortedFiles = getProductFilesSorted(book, outputAudioFilename); - - var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.'); - - // audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext - var audioFileName = FileUtility.GetValidFilename(destinationDir, book.Title, musicFileExt, book.AudibleProductId); - - bool movedAudioFile = false; - foreach (var f in sortedFiles) - { - var isAudio = AudibleFileStorage.Audio.IsFileTypeMatch(f); - var dest - = isAudio - ? Path.Join(destinationDir, f.Name) - // non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext + "]." + non_audio_ext - : FileUtility.GetValidFilename(destinationDir, book.Title, f.Extension, book.AudibleProductId, musicFileExt); - - if (Path.GetExtension(dest).Trim('.').ToLower() == "cue") - Cue.UpdateFileName(f, audioFileName); - - File.Move(f.FullName, dest); - if (isAudio) - FilePathCache.Upsert(book.AudibleProductId, FileType.Audio, dest); - - movedAudioFile |= AudibleFileStorage.Audio.IsFileTypeMatch(f); - } - - AudibleFileStorage.Audio.Refresh(); - - return movedAudioFile; - } - - private static List getProductFilesSorted(Book product, string outputAudioFilename) - { - // files are: temp path\author\[asin].ext - var m4bDir = new FileInfo(outputAudioFilename).Directory; - var files = m4bDir - .EnumerateFiles() - .Where(f => f.Name.ContainsInsensitive(product.AudibleProductId)) - .ToList(); - - // move audio files to the end of the collection so these files are moved last - var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f)); - var sortedFiles = files - .Except(musicFiles) - .Concat(musicFiles) - .ToList(); - - return sortedFiles; - } - - private static void validate(LibraryBook libraryBook) + private static void downloadValidation(LibraryBook libraryBook) { string errorString(string field) => $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book."; @@ -208,11 +156,56 @@ namespace FileLiberator throw new Exception(errorString("Locale")); } - public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists; - - public override void Cancel() + private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e) { - abDownloader?.Cancel(); + if (e is null && Configuration.Instance.AllowLibationFixup) + OnRequestCoverArt(abDownloader.SetCoverArt); + + if (e is not null) + OnCoverImageDiscovered(e); + } + + /// Move new files to 'Books' directory + /// True if audiobook file(s) were successfully created and can be located on disk. Else false. + private static bool moveFilesToBooksDir(Book book, List entries) + { + // create final directory. move each file into it. MOVE AUDIO FILE LAST + // new dir: safetitle_limit50char + " [" + productId + "]" + // TODO make this method handle multiple audio files or a single audio file. + var destinationDir = AudibleFileStorage.Audio.GetDestDir(book.Title, book.AudibleProductId); + Directory.CreateDirectory(destinationDir); + + var music = entries.FirstOrDefault(f => f.FileType == FileType.Audio); + + if (music == default) + return false; + + var musicFileExt = Path.GetExtension(music.Path).Trim('.'); + + // audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext + var audioFileName = FileUtility.GetValidFilename(destinationDir, book.Title, musicFileExt, book.AudibleProductId); + + foreach (var entry in entries) + { + var fileInfo = new FileInfo(entry.Path); + + var isAudio = entry.FileType == FileType.Audio; + var dest + = isAudio + ? Path.Join(destinationDir, fileInfo.Name) + // non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext + "]." + non_audio_ext + : FileUtility.GetValidFilename(destinationDir, book.Title, fileInfo.Extension, book.AudibleProductId, musicFileExt); + + if (Path.GetExtension(dest).Trim('.').ToLower() == "cue") + Cue.UpdateFileName(fileInfo, audioFileName); + + File.Move(fileInfo.FullName, dest); + FilePathCache.Insert(book.AudibleProductId, dest); + } + + AudibleFileStorage.Audio.Refresh(); + + return true; } } } diff --git a/FileLiberator/DownloadFile.cs b/FileLiberator/DownloadFile.cs index 2decfd58..0ed0492b 100644 --- a/FileLiberator/DownloadFile.cs +++ b/FileLiberator/DownloadFile.cs @@ -20,7 +20,7 @@ namespace FileLiberator try { var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress); - OnFileCreated("Upgrade", FileManager.FileType.Zip, actualDownloadedFilePath); + OnFileCreated("Upgrade", actualDownloadedFilePath); return actualDownloadedFilePath; } finally diff --git a/FileLiberator/DownloadPdf.cs b/FileLiberator/DownloadPdf.cs index 2b4f4ff5..e286eff8 100644 --- a/FileLiberator/DownloadPdf.cs +++ b/FileLiberator/DownloadPdf.cs @@ -47,7 +47,7 @@ namespace FileLiberator return Path.Combine(existingPath, Path.GetFileName(file)); var full = FileUtility.GetValidFilename( - AudibleFileStorage.PdfStorageDirectory, + AudibleFileStorage.PdfDirectory, libraryBook.Book.Title, Path.GetExtension(file), libraryBook.Book.AudibleProductId); @@ -72,7 +72,7 @@ namespace FileLiberator var client = new HttpClient(); var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress); - OnFileCreated(libraryBook.Book.AudibleProductId, FileType.PDF, actualDownloadedFilePath); + OnFileCreated(libraryBook.Book.AudibleProductId, actualDownloadedFilePath); OnStatusUpdate(actualDownloadedFilePath); return actualDownloadedFilePath; diff --git a/FileLiberator/Streamable.cs b/FileLiberator/Streamable.cs index d86377d0..52bf7cbf 100644 --- a/FileLiberator/Streamable.cs +++ b/FileLiberator/Streamable.cs @@ -10,7 +10,7 @@ namespace FileLiberator public event EventHandler StreamingTimeRemaining; public event EventHandler StreamingCompleted; /// Fired when a file is successfully saved to disk - public event EventHandler<(string id, FileManager.FileType type, string path)> FileCreated; + public event EventHandler<(string id, string path)> FileCreated; protected void OnStreamingBegin(string filePath) { @@ -34,11 +34,11 @@ namespace FileLiberator StreamingCompleted?.Invoke(this, filePath); } - protected void OnFileCreated(string productId, FileManager.FileType type, string path) + protected void OnFileCreated(string id, string path) { - Serilog.Log.Logger.Information("File created {@DebugInfo}", new { Name = nameof(FileCreated), productId, TypeId = (int)type, TypeName = type.ToString(), path }); - FileManager.FilePathCache.Upsert(productId, type, path); - FileCreated?.Invoke(this, (productId, type, path)); + Serilog.Log.Logger.Information("File created {@DebugInfo}", new { Name = nameof(FileCreated), id, path }); + FileManager.FilePathCache.Insert(id, path); + FileCreated?.Invoke(this, (id, path)); } } } diff --git a/FileManager/AudibleFileStorage.cs b/FileManager/AudibleFileStorage.cs index 4f5457e8..5bbb30b5 100644 --- a/FileManager/AudibleFileStorage.cs +++ b/FileManager/AudibleFileStorage.cs @@ -3,28 +3,22 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using Dinah.Core; -using Dinah.Core.Collections.Generic; namespace FileManager { - public enum FileType { Unknown, Audio, AAXC, PDF, Zip } + public abstract class AudibleFileStorage + { + protected abstract string GetFilePathCustom(string productId); - public abstract class AudibleFileStorage : Enumeration - { - protected abstract string[] Extensions { get; } - public abstract string StorageDirectory { get; } + #region static + public static string DownloadsInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName; + public static string DecryptInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName; + public static string PdfDirectory => BooksDirectory; - public static string DownloadsInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName; - public static string DecryptInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName; - - public static string PdfStorageDirectory => BooksDirectory; - - private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage(); + private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage(); public static bool AaxcExists(string productId) => AAXC.Exists(productId); - #region static - public static AudioFileStorage Audio { get; } = new AudioFileStorage(); + public static AudioFileStorage Audio { get; } = new AudioFileStorage(); public static string BooksDirectory { @@ -35,69 +29,60 @@ namespace FileManager return Directory.CreateDirectory(Configuration.Instance.Books).FullName; } } - - private static object bookDirectoryFilesLocker { get; } = new(); - internal static BackgroundFileSystem BookDirectoryFiles { get; set; } #endregion #region instance - public FileType FileType => (FileType)Value; + private FileType FileType { get; } + private string regexTemplate { get; } - protected IEnumerable extensions_noDots { get; } - private string extAggr { get; } + protected AudibleFileStorage(FileType fileType) + { + FileType = fileType; - protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString()) - { - extensions_noDots = Extensions.Select(ext => ext.ToLower().Trim('.')).ToList(); - extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}"); - BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); + var extAggr = FileTypes.GetExtensions(FileType).Aggregate((a, b) => $"{a}|{b}"); + regexTemplate = $@"{{0}}.*?\.({extAggr})$"; } protected string GetFilePath(string productId) { - var cachedFile = FilePathCache.GetPath(productId, FileType); + // primary lookup + var cachedFile = FilePathCache.GetFirstPath(productId, FileType); if (cachedFile != null) return cachedFile; - var regex = new Regex($@"{productId}.*?\.({extAggr})$", RegexOptions.IgnoreCase); + // secondary lookup attempt + var firstOrNull = GetFilePathCustom(productId); + if (firstOrNull is not null) + FilePathCache.Insert(productId, firstOrNull); - string firstOrNull; + return firstOrNull; + } - if (StorageDirectory == BooksDirectory) - { - //If user changed the BooksDirectory, reinitialize. - lock (bookDirectoryFilesLocker) - if (StorageDirectory != BookDirectoryFiles.RootDirectory) - BookDirectoryFiles = new BackgroundFileSystem(StorageDirectory, "*.*", SearchOption.AllDirectories); - - firstOrNull = BookDirectoryFiles.FindFile(regex); - } - else - { - firstOrNull = - Directory - .EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories) - .FirstOrDefault(s => regex.IsMatch(s)); - } - - if (firstOrNull is not null) - FilePathCache.Upsert(productId, FileType, firstOrNull); - - return firstOrNull; + protected Regex GetBookSearchRegex(string productId) + { + var pattern = string.Format(regexTemplate, productId); + return new Regex(pattern, RegexOptions.IgnoreCase); } #endregion } public class AudioFileStorage : AudibleFileStorage { - protected override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac" }; + private static BackgroundFileSystem BookDirectoryFiles { get; set; } + private static object bookDirectoryFilesLocker { get; } = new(); + protected override string GetFilePathCustom(string productId) + { + // If user changed the BooksDirectory: reinitialize + lock (bookDirectoryFilesLocker) + if (BooksDirectory != BookDirectoryFiles.RootDirectory) + BookDirectoryFiles = new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); - // we always want to use the latest config value, therefore - // - DO use 'get' arrow "=>" - // - do NOT use assign "=" - public override string StorageDirectory => BooksDirectory; + var regex = GetBookSearchRegex(productId); + return BookDirectoryFiles.FindFile(regex); + } - public AudioFileStorage() : base(FileType.Audio) { } + internal AudioFileStorage() : base(FileType.Audio) + => BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); public void Refresh() => BookDirectoryFiles.RefreshFiles(); @@ -109,33 +94,25 @@ namespace FileManager = underscoreIndex < 4 ? title : title.Substring(0, underscoreIndex); - var finalDir = FileUtility.GetValidFilename(StorageDirectory, titleDir, null, asin); + var finalDir = FileUtility.GetValidFilename(BooksDirectory, titleDir, null, asin); return finalDir; } - public bool IsFileTypeMatch(FileInfo fileInfo) - => extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.')); - public string GetPath(string productId) => GetFilePath(productId); } - public class AaxcFileStorage : AudibleFileStorage + internal class AaxcFileStorage : AudibleFileStorage { - protected override string[] Extensions { get; } = new[] { "aaxc" }; + protected override string GetFilePathCustom(string productId) + { + var regex = GetBookSearchRegex(productId); + return Directory + .EnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories) + .FirstOrDefault(s => regex.IsMatch(s)); + } - // we always want to use the latest config value, therefore - // - DO use 'get' arrow "=>" - // - do NOT use assign "=" - public override string StorageDirectory => DownloadsInProgress; + internal AaxcFileStorage() : base(FileType.AAXC) { } - public AaxcFileStorage() : base(FileType.AAXC) { } - - /// - /// Example for full books: - /// Search recursively in _books directory. Full book exists if either are true - /// - a directory name has the product id and an audio file is immediately inside - /// - any audio filename contains the product id - /// public bool Exists(string productId) => GetFilePath(productId) != null; } } diff --git a/FileManager/BackgroundFileSystem.cs b/FileManager/BackgroundFileSystem.cs index f999d4cb..663c702a 100644 --- a/FileManager/BackgroundFileSystem.cs +++ b/FileManager/BackgroundFileSystem.cs @@ -10,7 +10,7 @@ namespace FileManager /// /// Tracks actual locations of files. This is especially useful for clicking button to navigate to the book's files. /// - /// Note: this is no longer how Libation manages "Liberated" state. That is not statefully managed in the database. + /// Note: this is no longer how Libation manages "Liberated" state. That is now statefully managed in the database. /// This paradigm is what allows users to manually choose to not download books. Also allows them to manually toggle /// this state and download again. /// diff --git a/FileManager/FilePathCache.cs b/FileManager/FilePathCache.cs index c710740e..2d2dd5d6 100644 --- a/FileManager/FilePathCache.cs +++ b/FileManager/FilePathCache.cs @@ -8,14 +8,13 @@ using Newtonsoft.Json; namespace FileManager { public static class FilePathCache - { + { + public record CacheEntry(string Id, FileType FileType, string Path); + private const string FILENAME = "FileLocations.json"; - internal class CacheEntry - { - public string Id { get; set; } - public FileType FileType { get; set; } - public string Path { get; set; } - } + + public static event EventHandler Inserted; + public static event EventHandler Removed; private static Cache cache { get; } = new Cache(); @@ -31,48 +30,51 @@ namespace FileManager } } - public static bool Exists(string id, FileType type) => GetPath(id, type) != null; + public static bool Exists(string id, FileType type) => GetFirstPath(id, type) != null; - public static string GetPath(string id, FileType type) - { - var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type); + public static List<(FileType fileType, string path)> GetFiles(string id) + => getEntries(entry => entry.Id == id) + .Select(entry => (entry.FileType, entry.Path)) + .ToList(); - if (entry == null) - return null; + public static string GetFirstPath(string id, FileType type) + => getEntries(entry => entry.Id == id && entry.FileType == type) + .FirstOrDefault() + ?.Path; - if (!File.Exists(entry.Path)) - { - remove(entry); - return null; - } - - return entry.Path; - } - - private static void remove(CacheEntry entry) + private static List getEntries(Func predicate) { - cache.Remove(entry); - save(); + var entries = cache.Where(predicate).ToList(); + if (entries is null || !entries.Any()) + return null; + + remove(entries.Where(e => !File.Exists(e.Path)).ToList()); + + return entries; } - public static void Upsert(string id, FileType type, string path) + private static void remove(List entries) { - if (!File.Exists(path)) + if (entries is null) + return; + + lock (locker) { - // file not found can happen after rapid move - System.Threading.Thread.Sleep(100); - - if (!File.Exists(path)) - throw new FileNotFoundException($"Cannot add path to cache. File not found. Id={id} FileType={type}", path); + foreach (var entry in entries) + { + cache.Remove(entry); + Removed?.Invoke(null, entry); + } + save(); } + } - var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type); - - if (entry is null) - cache.Add(new CacheEntry { Id = id, FileType = type, Path = path }); - else - entry.Path = path; - + public static void Insert(string id, string path) + { + var type = FileTypes.GetFileTypeFromPath(path); + var entry = new CacheEntry(id, type, path); + cache.Add(entry); + Inserted?.Invoke(null, entry); save(); } diff --git a/FileManager/FileTypes.cs b/FileManager/FileTypes.cs new file mode 100644 index 00000000..25609145 --- /dev/null +++ b/FileManager/FileTypes.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace FileManager +{ + public enum FileType { Unknown, Audio, AAXC, PDF, Zip, Cue } + + public static class FileTypes + { + private static Dictionary dic => new() + { + ["aaxc"] = FileType.AAXC, + ["cue"] = FileType.Cue, + ["pdf"] = FileType.PDF, + ["zip"] = FileType.Zip, + + ["aac"] = FileType.Audio, + ["flac"] = FileType.Audio, + ["m4a"] = FileType.Audio, + ["m4b"] = FileType.Audio, + ["mp3"] = FileType.Audio, + ["mp4"] = FileType.Audio, + ["ogg"] = FileType.Audio, + }; + + public static FileType GetFileTypeFromPath(string path) + => dic.TryGetValue(Path.GetExtension(path).ToLower().Trim('.'), out var fileType) + ? fileType + : FileType.Unknown; + + public static List GetExtensions(FileType fileType) + => dic + .Where(kvp => kvp.Value == fileType) + .Select(kvp => kvp.Key) + .ToList(); + } +}