From aa56bb74a1d21dc85ce39d0d08fcf37f09f29779 Mon Sep 17 00:00:00 2001 From: Robert McRackan Date: Fri, 20 Aug 2021 20:51:37 -0400 Subject: [PATCH] refactor out most of TransitionalFileLocator. Almost done with new stateful is-liberated paradigm --- ApplicationServices/LibraryCommands.cs | 6 +- .../TransitionalFileLocator.cs | 25 ------ DataLayer/EfClasses/Book.cs | 19 +++++ FileLiberator/DownloadDecryptBook.cs | 8 +- FileLiberator/DownloadPdf.cs | 6 +- FileManager/AudibleFileStorage.cs | 79 ++++++++----------- FileManager/BackgroundFileSystem.cs | 9 ++- FileManager/FilePathCache.cs | 5 +- LibationLauncher/LibationLauncher.csproj | 2 +- LibationLauncher/Program.cs | 22 ++---- LibationSearchEngine/SearchEngine.cs | 4 +- LibationWinForms/ProductsGrid.cs | 2 +- REFERENCE.txt | 2 +- 13 files changed, 82 insertions(+), 107 deletions(-) diff --git a/ApplicationServices/LibraryCommands.cs b/ApplicationServices/LibraryCommands.cs index b27d5693..52a8d3d2 100644 --- a/ApplicationServices/LibraryCommands.cs +++ b/ApplicationServices/LibraryCommands.cs @@ -259,13 +259,13 @@ namespace ApplicationServices // below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those... public static LiberatedState Liberated_Status(Book book) - => TransitionalFileLocator.Audio_Exists(book) ? LiberatedState.Liberated - : TransitionalFileLocator.AAXC_Exists(book) ? LiberatedState.PartialDownload + => book.Audio_Exists ? LiberatedState.Liberated + : FileManager.AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedState.PartialDownload : LiberatedState.NotDownloaded; public static PdfState Pdf_Status(Book book) => !book.Supplements.Any() ? PdfState.NoPdf - : TransitionalFileLocator.PDF_Exists(book) ? PdfState.Downloaded + : book.PDF_Exists ? PdfState.Downloaded : PdfState.NotDownloaded; public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int pdfsDownloaded, int pdfsNotDownloaded) { } diff --git a/ApplicationServices/TransitionalFileLocator.cs b/ApplicationServices/TransitionalFileLocator.cs index e560e56b..aabf6189 100644 --- a/ApplicationServices/TransitionalFileLocator.cs +++ b/ApplicationServices/TransitionalFileLocator.cs @@ -17,30 +17,5 @@ namespace ApplicationServices return AudibleFileStorage.Audio.GetPath(book.AudibleProductId); } - - public static bool PDF_Exists(Book book) - { - var status = book?.UserDefinedItem?.PdfStatus; - if (status.HasValue && status.Value == LiberatedStatus.Liberated) - return true; - - return AudibleFileStorage.PDF.Exists(book.AudibleProductId); - } - - public static bool Audio_Exists(Book book) - { - var status = book?.UserDefinedItem?.BookStatus; - // true since Error == libhack - if (status.HasValue && status.Value != LiberatedStatus.NotLiberated) - return true; - - return AudibleFileStorage.Audio.Exists(book.AudibleProductId); - } - - public static bool AAXC_Exists(Book book) - { - // this one will actually stay the same. centralizing helps with organization in the interim though - return AudibleFileStorage.AAXC.Exists(book.AudibleProductId); - } } } diff --git a/DataLayer/EfClasses/Book.cs b/DataLayer/EfClasses/Book.cs index 3a566870..c9f87c5b 100644 --- a/DataLayer/EfClasses/Book.cs +++ b/DataLayer/EfClasses/Book.cs @@ -51,6 +51,25 @@ namespace DataLayer // is owned, not optional 1:1 public UserDefinedItem UserDefinedItem { get; private set; } + // UserDefinedItem convenience properties + public bool Audio_Exists + { + get + { + var status = UserDefinedItem?.BookStatus; + // true since Error == libhack + return status.HasValue && status.Value != LiberatedStatus.NotLiberated; + } + } + public bool PDF_Exists + { + get + { + var status = UserDefinedItem?.PdfStatus; + return (status.HasValue && status.Value == LiberatedStatus.Liberated); + } + } + // is owned, not optional 1:1 /// The product's aggregate community rating public Rating Rating { get; private set; } = new Rating(0, 0, 0); diff --git a/FileLiberator/DownloadDecryptBook.cs b/FileLiberator/DownloadDecryptBook.cs index d8596ae3..1dfcf3c2 100644 --- a/FileLiberator/DownloadDecryptBook.cs +++ b/FileLiberator/DownloadDecryptBook.cs @@ -37,7 +37,7 @@ namespace FileLiberator try { - if (ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book)) + if (libraryBook.Book.Audio_Exists) return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook); @@ -49,8 +49,7 @@ namespace FileLiberator // moves files and returns dest dir _ = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename); - var finalAudioExists = ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book); - if (!finalAudioExists) + if (!libraryBook.Book.Audio_Exists) return new StatusHandler { "Cannot find final audio file after decryption" }; // only need to update if success. if failure, it will remain at 0 == NotLiberated @@ -222,8 +221,7 @@ namespace FileLiberator throw new Exception(errorString("Locale")); } - public bool Validate(LibraryBook libraryBook) - => !ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book); + public bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists; public void Cancel() { diff --git a/FileLiberator/DownloadPdf.cs b/FileLiberator/DownloadPdf.cs index 8b496011..809775d8 100644 --- a/FileLiberator/DownloadPdf.cs +++ b/FileLiberator/DownloadPdf.cs @@ -15,7 +15,7 @@ namespace FileLiberator { public override bool Validate(LibraryBook libraryBook) => !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook)) - && !ApplicationServices.TransitionalFileLocator.PDF_Exists(libraryBook.Book); + && !libraryBook.Book.PDF_Exists; public override async Task ProcessItemAsync(LibraryBook libraryBook) { @@ -39,7 +39,7 @@ namespace FileLiberator return Path.Combine(existingPath, Path.GetFileName(file)); var full = FileUtility.GetValidFilename( - AudibleFileStorage.PDF.StorageDirectory, + AudibleFileStorage.PdfStorageDirectory, libraryBook.Book.Title, Path.GetExtension(file), libraryBook.Book.AudibleProductId); @@ -61,7 +61,7 @@ namespace FileLiberator } private static StatusHandler verifyDownload(LibraryBook libraryBook) - => !ApplicationServices.TransitionalFileLocator.PDF_Exists(libraryBook.Book) + => !libraryBook.Book.PDF_Exists ? new StatusHandler { "Downloaded PDF cannot be found" } : new StatusHandler(); } diff --git a/FileManager/AudibleFileStorage.cs b/FileManager/AudibleFileStorage.cs index 6af95a17..1c20b78c 100644 --- a/FileManager/AudibleFileStorage.cs +++ b/FileManager/AudibleFileStorage.cs @@ -15,14 +15,14 @@ namespace FileManager protected abstract string[] Extensions { get; } public abstract string StorageDirectory { get; } + 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; + public static bool AaxcExists(string productId) => AAXC.Exists(productId); + #region static public static AudioFileStorage Audio { get; } = new AudioFileStorage(); - public static AudibleFileStorage AAXC { get; } = new AaxcFileStorage(); - public static AudibleFileStorage PDF { get; } = new PdfFileStorage(); - - 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 AaxcFileStorage AAXC { get; } = new AaxcFileStorage(); public static string BooksDirectory { @@ -34,13 +34,13 @@ namespace FileManager } } - private static BackgroundFileSystem BookDirectoryFiles { get; set; } + internal static BackgroundFileSystem BookDirectoryFiles { get; set; } #endregion #region instance public FileType FileType => (FileType)Value; - private IEnumerable extensions_noDots { get; } + protected IEnumerable extensions_noDots { get; } private string extAggr { get; } protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString()) @@ -50,19 +50,6 @@ namespace FileManager BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); } - public void Refresh() - { - BookDirectoryFiles.RefreshFiles(); - } - - /// - /// 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) => GetPath(productId) != null; - public string GetPath(string productId) { var cachedFile = FilePathCache.GetPath(productId, FileType); @@ -103,21 +90,6 @@ namespace FileManager FilePathCache.Upsert(productId, FileType, firstOrNull); return firstOrNull; } - - public string GetDestDir(string title, string asin) - { - // to prevent the paths from getting too long, we don't need after the 1st ":" for the folder - var underscoreIndex = title.IndexOf(':'); - var titleDir - = underscoreIndex < 4 - ? title - : title.Substring(0, underscoreIndex); - var finalDir = FileUtility.GetValidFilename(StorageDirectory, titleDir, null, asin); - return finalDir; - } - - public bool IsFileTypeMatch(FileInfo fileInfo) - => extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.')); #endregion } @@ -134,6 +106,8 @@ namespace FileManager public AudioFileStorage() : base(FileType.Audio) { } + public void Refresh() => BookDirectoryFiles.RefreshFiles(); + public string CreateSkipFile(string title, string asin, string contents = null) { var destinationDir = GetDestDir(title, asin); @@ -144,6 +118,21 @@ namespace FileManager return path; } + + public string GetDestDir(string title, string asin) + { + // to prevent the paths from getting too long, we don't need after the 1st ":" for the folder + var underscoreIndex = title.IndexOf(':'); + var titleDir + = underscoreIndex < 4 + ? title + : title.Substring(0, underscoreIndex); + var finalDir = FileUtility.GetValidFilename(StorageDirectory, titleDir, null, asin); + return finalDir; + } + + public bool IsFileTypeMatch(FileInfo fileInfo) + => extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.')); } public class AaxcFileStorage : AudibleFileStorage @@ -156,17 +145,13 @@ namespace FileManager public override string StorageDirectory => DownloadsInProgress; public AaxcFileStorage() : base(FileType.AAXC) { } - } - public class PdfFileStorage : AudibleFileStorage - { - protected override string[] Extensions { get; } = new[] { "pdf", "zip" }; - - // we always want to use the latest config value, therefore - // - DO use 'get' arrow "=>" - // - do NOT use assign "=" - public override string StorageDirectory => BooksDirectory; - - public PdfFileStorage() : base(FileType.PDF) { } + /// + /// 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) => GetPath(productId) != null; } } diff --git a/FileManager/BackgroundFileSystem.cs b/FileManager/BackgroundFileSystem.cs index 1ed670ac..f4a84167 100644 --- a/FileManager/BackgroundFileSystem.cs +++ b/FileManager/BackgroundFileSystem.cs @@ -9,7 +9,14 @@ using System.Threading.Tasks; namespace FileManager { - class BackgroundFileSystem + /// + /// 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. + /// This paradigm is what allows users to manually choose to not download books. Also allows them to manually toggle + /// this state and download again. + /// + internal class BackgroundFileSystem { public string RootDirectory { get; private set; } public string SearchPattern { get; private set; } diff --git a/FileManager/FilePathCache.cs b/FileManager/FilePathCache.cs index bfd175a2..ea4070d5 100644 --- a/FileManager/FilePathCache.cs +++ b/FileManager/FilePathCache.cs @@ -9,6 +9,7 @@ namespace FileManager { public static class FilePathCache { + private const string FILENAME = "FileLocations.json"; internal class CacheEntry { public string Id { get; set; } @@ -18,7 +19,7 @@ namespace FileManager private static Cache cache { get; } = new Cache(); - private static string jsonFile => Path.Combine(Configuration.Instance.LibationFiles, "FilePaths.json"); + private static string jsonFile => Path.Combine(Configuration.Instance.LibationFiles, FILENAME); static FilePathCache() { @@ -84,7 +85,7 @@ namespace FileManager try { resave(); } catch (IOException ex) { - Serilog.Log.Logger.Error(ex, "Error saving FilePaths.json"); + Serilog.Log.Logger.Error(ex, $"Error saving {FILENAME}"); throw; } } diff --git a/LibationLauncher/LibationLauncher.csproj b/LibationLauncher/LibationLauncher.csproj index ee621d18..a9921d8b 100644 --- a/LibationLauncher/LibationLauncher.csproj +++ b/LibationLauncher/LibationLauncher.csproj @@ -13,7 +13,7 @@ win-x64 - 5.5.1.5 + 5.5.1.11 diff --git a/LibationLauncher/Program.cs b/LibationLauncher/Program.cs index bcfb1f4d..07897927 100644 --- a/LibationLauncher/Program.cs +++ b/LibationLauncher/Program.cs @@ -52,7 +52,7 @@ namespace LibationLauncher migrate_to_v5_0_0(config); migrate_to_v5_2_0__post_config(config); - //migrate_to_v5_4_1(config);// comment out until after vacation + migrate_to_v5_5_0(config); ensureSerilogConfig(config); configureLogging(config); @@ -233,19 +233,11 @@ namespace LibationLauncher } #endregion - #region migrate to v5.4.1 see comment - // this 'migration' is a bit different. it intentionally runs each time Libation is started. its job will be fulfilled when I eventually - // implement the portion which removes FilePaths.json, at which time this method will be a proper migration - // - // I'm iterating through safe steps toward getting rid of the live scanner except to track audiobook files as a convenience - // such as clicking the stop light to open its location. live scanning will be replaced with state tracking in the database. - - // FilePaths.json => db. long running. fire and forget - private static void migrate_to_v5_4_1(Configuration config) - => new System.Threading.Thread(() => migrate_to_v5_4_1_thread(config)) { IsBackground = true }.Start(); - private static void migrate_to_v5_4_1_thread(Configuration config) + #region migrate to v5.5.0. FilePaths.json => db. long running. fire and forget + private static void migrate_to_v5_5_0(Configuration config) + => new System.Threading.Thread(() => migrate_to_v5_5_0_thread(config)) { IsBackground = true }.Start(); + private static void migrate_to_v5_5_0_thread(Configuration config) { - var debugStopwatch = System.Diagnostics.Stopwatch.StartNew(); try { var filePaths = Path.Combine(config.LibationFiles, "FilePaths.json"); @@ -289,13 +281,13 @@ namespace LibationLauncher } context.SaveChanges(); + + File.Delete(filePaths); } catch (Exception ex) { Log.Logger.Error(ex, "Error attempting to insert FilePaths into db"); } - debugStopwatch.Stop(); - var debugTotal = debugStopwatch.Elapsed; } #endregion diff --git a/LibationSearchEngine/SearchEngine.cs b/LibationSearchEngine/SearchEngine.cs index 4df7956f..550ef809 100644 --- a/LibationSearchEngine/SearchEngine.cs +++ b/LibationSearchEngine/SearchEngine.cs @@ -139,9 +139,7 @@ namespace LibationSearchEngine return authors.Intersect(narrators).Any(); } - private static bool isLiberated(Book book) - => book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated - || AudibleFileStorage.Audio.Exists(book.AudibleProductId); + private static bool isLiberated(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated; private static bool liberatedError(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Error; // use these common fields in the "all" default search field diff --git a/LibationWinForms/ProductsGrid.cs b/LibationWinForms/ProductsGrid.cs index 1aea5520..1c156020 100644 --- a/LibationWinForms/ProductsGrid.cs +++ b/LibationWinForms/ProductsGrid.cs @@ -77,7 +77,7 @@ namespace LibationWinForms var libraryBook = liveGridEntry.LibraryBook; // liberated: open explorer to file - if (TransitionalFileLocator.Audio_Exists(libraryBook.Book)) + if (libraryBook.Book.Audio_Exists) { var filePath = TransitionalFileLocator.Audio_GetPath(libraryBook.Book); if (!Go.To.File(filePath)) diff --git a/REFERENCE.txt b/REFERENCE.txt index d66c5a9c..0b5edb65 100644 --- a/REFERENCE.txt +++ b/REFERENCE.txt @@ -67,7 +67,7 @@ alternate book id (eg BK_RAND_006061) is called 'sku' , 'sku_lite' , 'prod_id' , -- begin SOLUTION LAYOUT --------------------------------------------------------------------------------------------------------------------- do NOT combine jsons for - audible-scraped persistence: library, book details -- libation-generated persistence: FilePaths.json +- libation-generated persistence: FileLocations.json - user-defined persistence: BookTags.json -- end SOLUTION LAYOUT ---------------------------------------------------------------------------------------------------------------------