refactor out most of TransitionalFileLocator. Almost done with new stateful is-liberated paradigm

This commit is contained in:
Robert McRackan 2021-08-20 20:51:37 -04:00
parent a639857ec6
commit aa56bb74a1
13 changed files with 82 additions and 107 deletions

View File

@ -259,13 +259,13 @@ namespace ApplicationServices
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those... // 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) public static LiberatedState Liberated_Status(Book book)
=> TransitionalFileLocator.Audio_Exists(book) ? LiberatedState.Liberated => book.Audio_Exists ? LiberatedState.Liberated
: TransitionalFileLocator.AAXC_Exists(book) ? LiberatedState.PartialDownload : FileManager.AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedState.PartialDownload
: LiberatedState.NotDownloaded; : LiberatedState.NotDownloaded;
public static PdfState Pdf_Status(Book book) public static PdfState Pdf_Status(Book book)
=> !book.Supplements.Any() ? PdfState.NoPdf => !book.Supplements.Any() ? PdfState.NoPdf
: TransitionalFileLocator.PDF_Exists(book) ? PdfState.Downloaded : book.PDF_Exists ? PdfState.Downloaded
: PdfState.NotDownloaded; : PdfState.NotDownloaded;
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int pdfsDownloaded, int pdfsNotDownloaded) { } public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int pdfsDownloaded, int pdfsNotDownloaded) { }

View File

@ -17,30 +17,5 @@ namespace ApplicationServices
return AudibleFileStorage.Audio.GetPath(book.AudibleProductId); 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);
}
} }
} }

View File

@ -51,6 +51,25 @@ namespace DataLayer
// is owned, not optional 1:1 // is owned, not optional 1:1
public UserDefinedItem UserDefinedItem { get; private set; } 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 // is owned, not optional 1:1
/// <summary>The product's aggregate community rating</summary> /// <summary>The product's aggregate community rating</summary>
public Rating Rating { get; private set; } = new Rating(0, 0, 0); public Rating Rating { get; private set; } = new Rating(0, 0, 0);

View File

@ -37,7 +37,7 @@ namespace FileLiberator
try try
{ {
if (ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book)) if (libraryBook.Book.Audio_Exists)
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook); var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook);
@ -49,8 +49,7 @@ namespace FileLiberator
// moves files and returns dest dir // moves files and returns dest dir
_ = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename); _ = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
var finalAudioExists = ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book); if (!libraryBook.Book.Audio_Exists)
if (!finalAudioExists)
return new StatusHandler { "Cannot find final audio file after decryption" }; return new StatusHandler { "Cannot find final audio file after decryption" };
// only need to update if success. if failure, it will remain at 0 == NotLiberated // 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")); throw new Exception(errorString("Locale"));
} }
public bool Validate(LibraryBook libraryBook) public bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;
=> !ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book);
public void Cancel() public void Cancel()
{ {

View File

@ -15,7 +15,7 @@ namespace FileLiberator
{ {
public override bool Validate(LibraryBook libraryBook) public override bool Validate(LibraryBook libraryBook)
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook)) => !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
&& !ApplicationServices.TransitionalFileLocator.PDF_Exists(libraryBook.Book); && !libraryBook.Book.PDF_Exists;
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook) public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
{ {
@ -39,7 +39,7 @@ namespace FileLiberator
return Path.Combine(existingPath, Path.GetFileName(file)); return Path.Combine(existingPath, Path.GetFileName(file));
var full = FileUtility.GetValidFilename( var full = FileUtility.GetValidFilename(
AudibleFileStorage.PDF.StorageDirectory, AudibleFileStorage.PdfStorageDirectory,
libraryBook.Book.Title, libraryBook.Book.Title,
Path.GetExtension(file), Path.GetExtension(file),
libraryBook.Book.AudibleProductId); libraryBook.Book.AudibleProductId);
@ -61,7 +61,7 @@ namespace FileLiberator
} }
private static StatusHandler verifyDownload(LibraryBook libraryBook) 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 { "Downloaded PDF cannot be found" }
: new StatusHandler(); : new StatusHandler();
} }

View File

@ -15,14 +15,14 @@ namespace FileManager
protected abstract string[] Extensions { get; } protected abstract string[] Extensions { get; }
public abstract string StorageDirectory { 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 #region static
public static AudioFileStorage Audio { get; } = new AudioFileStorage(); public static AudioFileStorage Audio { get; } = new AudioFileStorage();
public static AudibleFileStorage AAXC { get; } = new AaxcFileStorage(); public static AaxcFileStorage 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 string BooksDirectory public static string BooksDirectory
{ {
@ -34,13 +34,13 @@ namespace FileManager
} }
} }
private static BackgroundFileSystem BookDirectoryFiles { get; set; } internal static BackgroundFileSystem BookDirectoryFiles { get; set; }
#endregion #endregion
#region instance #region instance
public FileType FileType => (FileType)Value; public FileType FileType => (FileType)Value;
private IEnumerable<string> extensions_noDots { get; } protected IEnumerable<string> extensions_noDots { get; }
private string extAggr { get; } private string extAggr { get; }
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString()) protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
@ -50,19 +50,6 @@ namespace FileManager
BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
} }
public void Refresh()
{
BookDirectoryFiles.RefreshFiles();
}
/// <summary>
/// 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
/// </summary>
public bool Exists(string productId) => GetPath(productId) != null;
public string GetPath(string productId) public string GetPath(string productId)
{ {
var cachedFile = FilePathCache.GetPath(productId, FileType); var cachedFile = FilePathCache.GetPath(productId, FileType);
@ -103,21 +90,6 @@ namespace FileManager
FilePathCache.Upsert(productId, FileType, firstOrNull); FilePathCache.Upsert(productId, FileType, firstOrNull);
return 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 #endregion
} }
@ -134,6 +106,8 @@ namespace FileManager
public AudioFileStorage() : base(FileType.Audio) { } public AudioFileStorage() : base(FileType.Audio) { }
public void Refresh() => BookDirectoryFiles.RefreshFiles();
public string CreateSkipFile(string title, string asin, string contents = null) public string CreateSkipFile(string title, string asin, string contents = null)
{ {
var destinationDir = GetDestDir(title, asin); var destinationDir = GetDestDir(title, asin);
@ -144,6 +118,21 @@ namespace FileManager
return path; 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 public class AaxcFileStorage : AudibleFileStorage
@ -156,17 +145,13 @@ namespace FileManager
public override string StorageDirectory => DownloadsInProgress; public override string StorageDirectory => DownloadsInProgress;
public AaxcFileStorage() : base(FileType.AAXC) { } public AaxcFileStorage() : base(FileType.AAXC) { }
}
public class PdfFileStorage : AudibleFileStorage /// <summary>
{ /// Example for full books:
protected override string[] Extensions { get; } = new[] { "pdf", "zip" }; /// 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
// we always want to use the latest config value, therefore /// - any audio filename contains the product id
// - DO use 'get' arrow "=>" /// </summary>
// - do NOT use assign "=" public bool Exists(string productId) => GetPath(productId) != null;
public override string StorageDirectory => BooksDirectory;
public PdfFileStorage() : base(FileType.PDF) { }
} }
} }

View File

@ -9,7 +9,14 @@ using System.Threading.Tasks;
namespace FileManager namespace FileManager
{ {
class BackgroundFileSystem /// <summary>
/// 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.
/// </summary>
internal class BackgroundFileSystem
{ {
public string RootDirectory { get; private set; } public string RootDirectory { get; private set; }
public string SearchPattern { get; private set; } public string SearchPattern { get; private set; }

View File

@ -9,6 +9,7 @@ namespace FileManager
{ {
public static class FilePathCache public static class FilePathCache
{ {
private const string FILENAME = "FileLocations.json";
internal class CacheEntry internal class CacheEntry
{ {
public string Id { get; set; } public string Id { get; set; }
@ -18,7 +19,7 @@ namespace FileManager
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>(); private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
private static string jsonFile => Path.Combine(Configuration.Instance.LibationFiles, "FilePaths.json"); private static string jsonFile => Path.Combine(Configuration.Instance.LibationFiles, FILENAME);
static FilePathCache() static FilePathCache()
{ {
@ -84,7 +85,7 @@ namespace FileManager
try { resave(); } try { resave(); }
catch (IOException ex) catch (IOException ex)
{ {
Serilog.Log.Logger.Error(ex, "Error saving FilePaths.json"); Serilog.Log.Logger.Error(ex, $"Error saving {FILENAME}");
throw; throw;
} }
} }

View File

@ -13,7 +13,7 @@
<!-- <PublishSingleFile>true</PublishSingleFile> --> <!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>5.5.1.5</Version> <Version>5.5.1.11</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -52,7 +52,7 @@ namespace LibationLauncher
migrate_to_v5_0_0(config); migrate_to_v5_0_0(config);
migrate_to_v5_2_0__post_config(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); ensureSerilogConfig(config);
configureLogging(config); configureLogging(config);
@ -233,19 +233,11 @@ namespace LibationLauncher
} }
#endregion #endregion
#region migrate to v5.4.1 see comment #region migrate to v5.5.0. FilePaths.json => db. long running. fire and forget
// this 'migration' is a bit different. it intentionally runs each time Libation is started. its job will be fulfilled when I eventually private static void migrate_to_v5_5_0(Configuration config)
// implement the portion which removes FilePaths.json, at which time this method will be a proper migration => 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)
// 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)
{ {
var debugStopwatch = System.Diagnostics.Stopwatch.StartNew();
try try
{ {
var filePaths = Path.Combine(config.LibationFiles, "FilePaths.json"); var filePaths = Path.Combine(config.LibationFiles, "FilePaths.json");
@ -289,13 +281,13 @@ namespace LibationLauncher
} }
context.SaveChanges(); context.SaveChanges();
File.Delete(filePaths);
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Logger.Error(ex, "Error attempting to insert FilePaths into db"); Log.Logger.Error(ex, "Error attempting to insert FilePaths into db");
} }
debugStopwatch.Stop();
var debugTotal = debugStopwatch.Elapsed;
} }
#endregion #endregion

View File

@ -139,9 +139,7 @@ namespace LibationSearchEngine
return authors.Intersect(narrators).Any(); return authors.Intersect(narrators).Any();
} }
private static bool isLiberated(Book book) private static bool isLiberated(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated;
=> book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated
|| AudibleFileStorage.Audio.Exists(book.AudibleProductId);
private static bool liberatedError(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Error; private static bool liberatedError(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Error;
// use these common fields in the "all" default search field // use these common fields in the "all" default search field

View File

@ -77,7 +77,7 @@ namespace LibationWinForms
var libraryBook = liveGridEntry.LibraryBook; var libraryBook = liveGridEntry.LibraryBook;
// liberated: open explorer to file // liberated: open explorer to file
if (TransitionalFileLocator.Audio_Exists(libraryBook.Book)) if (libraryBook.Book.Audio_Exists)
{ {
var filePath = TransitionalFileLocator.Audio_GetPath(libraryBook.Book); var filePath = TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
if (!Go.To.File(filePath)) if (!Go.To.File(filePath))

View File

@ -67,7 +67,7 @@ alternate book id (eg BK_RAND_006061) is called 'sku' , 'sku_lite' , 'prod_id' ,
-- begin SOLUTION LAYOUT --------------------------------------------------------------------------------------------------------------------- -- begin SOLUTION LAYOUT ---------------------------------------------------------------------------------------------------------------------
do NOT combine jsons for do NOT combine jsons for
- audible-scraped persistence: library, book details - audible-scraped persistence: library, book details
- libation-generated persistence: FilePaths.json - libation-generated persistence: FileLocations.json
- user-defined persistence: BookTags.json - user-defined persistence: BookTags.json
-- end SOLUTION LAYOUT --------------------------------------------------------------------------------------------------------------------- -- end SOLUTION LAYOUT ---------------------------------------------------------------------------------------------------------------------