From b4aa22005115db94e9953866504a4619b886533b Mon Sep 17 00:00:00 2001 From: Mbucari Date: Mon, 12 Jun 2023 13:22:35 -0600 Subject: [PATCH 1/2] Refactor LibationSearchEngine --- .../Dialogs/SearchSyntaxDialog.axaml.cs | 10 +- Source/LibationSearchEngine/BookRule.cs | 21 ++ Source/LibationSearchEngine/IIndexRule.cs | 22 ++ .../IndexRuleCollection.cs | 28 ++ .../LibationSearchEngine/LibraryBookRule.cs | 21 ++ .../LibationSearchEngine/LuceneExtensions.cs | 63 +++- Source/LibationSearchEngine/QuerySanitizer.cs | 22 +- Source/LibationSearchEngine/SearchEngine.cs | 303 ++++-------------- .../Dialogs/SearchSyntaxDialog.cs | 11 +- 9 files changed, 235 insertions(+), 266 deletions(-) create mode 100644 Source/LibationSearchEngine/BookRule.cs create mode 100644 Source/LibationSearchEngine/IIndexRule.cs create mode 100644 Source/LibationSearchEngine/IndexRuleCollection.cs create mode 100644 Source/LibationSearchEngine/LibraryBookRule.cs diff --git a/Source/LibationAvalonia/Dialogs/SearchSyntaxDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/SearchSyntaxDialog.axaml.cs index 07992c0b..07ca798f 100644 --- a/Source/LibationAvalonia/Dialogs/SearchSyntaxDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/SearchSyntaxDialog.axaml.cs @@ -1,3 +1,5 @@ +using LibationSearchEngine; + namespace LibationAvalonia.Dialogs { public partial class SearchSyntaxDialog : DialogWindow @@ -18,7 +20,7 @@ Search for wizard of oz: title:""wizard of oz"" -" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchStringFields()); +" + string.Join("\r\n", SearchEngine.FieldIndexRules.StringFieldNames); NumberFields = @" Find books between 1-100 minutes long @@ -30,14 +32,14 @@ Find books published from 2020-1-1 to datepublished:[20200101 TO 20231231] -" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchNumberFields()); +" + string.Join("\r\n", SearchEngine.FieldIndexRules.NumberFieldNames); BoolFields = @" Find books that you haven't rated: -IsRated -" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchBoolFields()); +" + string.Join("\r\n", SearchEngine.FieldIndexRules.BoolFieldNames); IdFields = @" Alice's Adventures in @@ -49,7 +51,7 @@ All of these are synonyms for the ID field -" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchIdFields()); +" + string.Join("\r\n", SearchEngine.FieldIndexRules.IdFieldNames); DataContext = this; diff --git a/Source/LibationSearchEngine/BookRule.cs b/Source/LibationSearchEngine/BookRule.cs new file mode 100644 index 00000000..e7c296e1 --- /dev/null +++ b/Source/LibationSearchEngine/BookRule.cs @@ -0,0 +1,21 @@ +using DataLayer; +using System; +using System.Collections.ObjectModel; + +namespace LibationSearchEngine; + +public class BookRule : IIndexRule +{ + public FieldType FieldType { get; } + public Func ValueGetter { get; } + public ReadOnlyCollection FieldNames { get; } + + public BookRule(FieldType fieldType, Func valueGetter, params string[] fieldNames) + { + ValueGetter = valueGetter; + FieldType = fieldType; + FieldNames = new ReadOnlyCollection(fieldNames); + } + + public string GetValue(LibraryBook libraryBook) => ValueGetter(libraryBook.Book); +} diff --git a/Source/LibationSearchEngine/IIndexRule.cs b/Source/LibationSearchEngine/IIndexRule.cs new file mode 100644 index 00000000..dcaa441f --- /dev/null +++ b/Source/LibationSearchEngine/IIndexRule.cs @@ -0,0 +1,22 @@ +using DataLayer; +using System.Collections.ObjectModel; + +namespace LibationSearchEngine; + +public enum FieldType +{ + Bool, + String, + Number, + ID, + Raw +} + +public interface IIndexRule +{ + /// This rule's value type. + FieldType FieldType { get; } + /// All aliases of this search index rule + ReadOnlyCollection FieldNames { get; } + string GetValue(LibraryBook libraryBook); +} diff --git a/Source/LibationSearchEngine/IndexRuleCollection.cs b/Source/LibationSearchEngine/IndexRuleCollection.cs new file mode 100644 index 00000000..2a20dad1 --- /dev/null +++ b/Source/LibationSearchEngine/IndexRuleCollection.cs @@ -0,0 +1,28 @@ +using DataLayer; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace LibationSearchEngine; + +public class IndexRuleCollection : IEnumerable +{ + private readonly List rules = new(); + public IEnumerable IdFieldNames => rules.Where(x => x.FieldType is FieldType.ID).SelectMany(r => r.FieldNames); + public IEnumerable BoolFieldNames => rules.Where(x => x.FieldType is FieldType.Bool).SelectMany(r => r.FieldNames); + public IEnumerable StringFieldNames => rules.Where(x => x.FieldType is FieldType.String).SelectMany(r => r.FieldNames); + public IEnumerable NumberFieldNames => rules.Where(x => x.FieldType is FieldType.Number).SelectMany(r => r.FieldNames); + + public void Add(FieldType fieldType, Func getter, params string[] fieldNames) + => rules.Add(new LibraryBookRule(fieldType, getter, fieldNames)); + + public void Add(FieldType fieldType, Func getter, params string[] fieldNames) + => rules.Add(new BookRule(fieldType, getter, fieldNames)); + + public T GetRuleByFieldName(string fieldName) where T : IIndexRule + => (T)rules.SingleOrDefault(r => r.FieldNames.Any(n => n.Equals(fieldName, StringComparison.OrdinalIgnoreCase))); + + public IEnumerator GetEnumerator() => rules.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/Source/LibationSearchEngine/LibraryBookRule.cs b/Source/LibationSearchEngine/LibraryBookRule.cs new file mode 100644 index 00000000..6c9036b7 --- /dev/null +++ b/Source/LibationSearchEngine/LibraryBookRule.cs @@ -0,0 +1,21 @@ +using DataLayer; +using System; +using System.Collections.ObjectModel; + +namespace LibationSearchEngine; + +public class LibraryBookRule : IIndexRule +{ + public FieldType FieldType { get; } + public Func ValueGetter { get; } + public ReadOnlyCollection FieldNames { get; } + + public LibraryBookRule(FieldType fieldType, Func valueGetter, params string[] fieldNames) + { + ValueGetter = valueGetter; + FieldType = fieldType; + FieldNames = new ReadOnlyCollection(fieldNames); + } + + public string GetValue(LibraryBook libraryBook) => ValueGetter(libraryBook); +} diff --git a/Source/LibationSearchEngine/LuceneExtensions.cs b/Source/LibationSearchEngine/LuceneExtensions.cs index 20c3eaa9..0e09fe8d 100644 --- a/Source/LibationSearchEngine/LuceneExtensions.cs +++ b/Source/LibationSearchEngine/LuceneExtensions.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; +using DataLayer; using Lucene.Net.Analysis; using Lucene.Net.Documents; using Lucene.Net.QueryParsers; @@ -10,21 +10,58 @@ namespace LibationSearchEngine { // field names are case specific and, due to StandardAnalyzer, content is case INspecific internal static class LuceneExtensions - { - internal static void AddRaw(this Document document, string name, string value) - => document.Add(new Field(name, value, Field.Store.YES, Field.Index.NOT_ANALYZED)); + { + internal static void AddAnalyzed(this Document document, string name, string value) + { + if (value is not null) + document.Add(new Field(name.ToLowerInvariant(), value, Field.Store.YES, Field.Index.ANALYZED)); + } - internal static void AddAnalyzed(this Document document, string name, string value) - { - if (value is not null) - document.Add(new Field(name.ToLowerInvariant(), value, Field.Store.YES, Field.Index.ANALYZED)); - } + internal static void RemoveRule(this Document document, IIndexRule rule) + { + // fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY. + // ie: must remove old before adding new else will create unwanted duplicates. + foreach (var name in rule.FieldNames) + document.RemoveFields(name.ToLowerInvariant()); + } - internal static void AddNotAnalyzed(this Document document, string name, string value) - => document.Add(new Field(name.ToLowerInvariant(), value, Field.Store.YES, Field.Index.NOT_ANALYZED)); + internal static void AddIndexRule(this Document document, IIndexRule rule, LibraryBook libraryBook) + { + string value = rule.GetValue(libraryBook); - internal static void AddBool(this Document document, string name, bool value) - => document.Add(new Field(name.ToLowerInvariant(), value.ToString(), Field.Store.YES, Field.Index.ANALYZED_NO_NORMS)); + addIndexRule(document, rule, value); + } + + internal static void AddIndexRule(this Document document, BookRule rule, Book libraryBook) + { + addIndexRule(document, rule, rule.ValueGetter(libraryBook)); + } + + private static void addIndexRule(Document document, IIndexRule rule, string value) + { + if (value is null) return; + + foreach (var name in rule.FieldNames) + { + // fields are key value pairs and MULTIPLE FIELDS CAN HAVE THE SAME KEY. + // splitting authors and narrators and/or tags into multiple fields could be interesting research. + // it could allow for more advanced searches, or maybe it could break broad searches. + + // all searching should be lowercase + // external callers have the reasonable expectation that product id will be returned CASE SPECIFIC + var field = rule.FieldType switch + { + FieldType.Bool => new Field(name.ToLowerInvariant(), value, Field.Store.YES, Field.Index.ANALYZED_NO_NORMS), + FieldType.String => new Field(name.ToLowerInvariant(), value, Field.Store.YES, Field.Index.ANALYZED), + FieldType.Number => new Field(name.ToLowerInvariant(), value, Field.Store.YES, Field.Index.NOT_ANALYZED), + FieldType.ID => new Field(name.ToLowerInvariant(), value, Field.Store.YES, Field.Index.NOT_ANALYZED), + FieldType.Raw => new Field(name, value, Field.Store.YES, Field.Index.NOT_ANALYZED), + _ => throw new KeyNotFoundException(), + }; + + document.Add(field); + } + } internal static Query GetQuery(this Analyzer analyzer, string defaultField, string searchString) => new QueryParser(SearchEngine.Version, defaultField.ToLowerInvariant(), analyzer).Parse(searchString); diff --git a/Source/LibationSearchEngine/QuerySanitizer.cs b/Source/LibationSearchEngine/QuerySanitizer.cs index bd4acef0..f6020528 100644 --- a/Source/LibationSearchEngine/QuerySanitizer.cs +++ b/Source/LibationSearchEngine/QuerySanitizer.cs @@ -8,22 +8,20 @@ namespace LibationSearchEngine internal static class QuerySanitizer { private static readonly HashSet idTerms - = SearchEngine.idIndexRules.Keys - .Select(s => s.ToLowerInvariant()) - .ToHashSet(); + = SearchEngine.FieldIndexRules.IdFieldNames + .Select(n => n.ToLowerInvariant()) + .ToHashSet(); private static readonly HashSet boolTerms - = SearchEngine.boolIndexRules.Keys - .Select(s => s.ToLowerInvariant()) - .ToHashSet(); + = SearchEngine.FieldIndexRules.BoolFieldNames + .Select(n => n.ToLowerInvariant()) + .ToHashSet(); private static readonly HashSet fieldTerms - = SearchEngine.stringIndexRules.Keys - .Union(SearchEngine.numberIndexRules.Keys) - .Select(s => s.ToLowerInvariant()) - .Union(idTerms) - .Union(boolTerms) - .ToHashSet(); + = SearchEngine.FieldIndexRules + .SelectMany(r => r.FieldNames) + .Select(n => n.ToLowerInvariant()) + .ToHashSet(); internal static string Sanitize(string searchString, StandardAnalyzer analyzer) { diff --git a/Source/LibationSearchEngine/SearchEngine.cs b/Source/LibationSearchEngine/SearchEngine.cs index 83d7d588..872a4b1a 100644 --- a/Source/LibationSearchEngine/SearchEngine.cs +++ b/Source/LibationSearchEngine/SearchEngine.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; -using System.Text.RegularExpressions; using DataLayer; using Dinah.Core; using LibationFileManager; using Lucene.Net.Analysis.Standard; -using Lucene.Net.Analysis.Tokenattributes; using Lucene.Net.Documents; using Lucene.Net.Index; using Lucene.Net.Search; @@ -25,164 +22,47 @@ namespace LibationSearchEngine public const string ALL = "all"; #region index rules - // common fields used in the "all" default search field - public const string ALL_AUDIBLE_PRODUCT_ID = nameof(Book.AudibleProductId); - public const string ALL_TITLE = nameof(Book.Title); - public const string ALL_AUTHOR_NAMES = "AuthorNames"; - public const string ALL_NARRATOR_NAMES = "NarratorNames"; - public const string ALL_SERIES_NAMES = "SeriesNames"; - internal static ReadOnlyDictionary> idIndexRules { get; } - = new ReadOnlyDictionary>( - new Dictionary> - { - [nameof(Book.AudibleProductId)] = lb => lb.Book.AudibleProductId.ToLowerInvariant(), - ["ProductId"] = lb => lb.Book.AudibleProductId.ToLowerInvariant(), - ["Id"] = lb => lb.Book.AudibleProductId.ToLowerInvariant(), - ["ASIN"] = lb => lb.Book.AudibleProductId.ToLowerInvariant() - } - ); - - internal static ReadOnlyDictionary> stringIndexRules { get; } - = new ReadOnlyDictionary>( - new Dictionary> - { - [nameof(Book.Title)] = lb => lb.Book.Title, - [ALL_AUTHOR_NAMES] = lb => lb.Book.AuthorNames(), - ["Author"] = lb => lb.Book.AuthorNames(), - ["Authors"] = lb => lb.Book.AuthorNames(), - [ALL_NARRATOR_NAMES] = lb => lb.Book.NarratorNames(), - ["Narrator"] = lb => lb.Book.NarratorNames(), - ["Narrators"] = lb => lb.Book.NarratorNames(), - [nameof(Book.Publisher)] = lb => lb.Book.Publisher, - - [ALL_SERIES_NAMES] = lb => lb.Book.SeriesNames(), - ["Series"] = lb => lb.Book.SeriesNames(), - ["SeriesId"] = lb => string.Join(", ", lb.Book.SeriesLink.Select(s => s.Series.AudibleSeriesId)), - - ["CategoriesNames"] = lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()), - [nameof(Book.Category)] = lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()), - ["Categories"] = lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()), - ["CategoriesId"] = lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()), - ["CategoryId"] = lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()), - - [TAGS.FirstCharToUpper()] = lb => lb.Book.UserDefinedItem.Tags, - - ["Locale"] = lb => lb.Book.Locale, - ["Region"] = lb => lb.Book.Locale, - ["Account"] = lb => lb.Account, - ["Email"] = lb => lb.Account - } - ); - - internal static ReadOnlyDictionary> numberIndexRules { get; } - = new ReadOnlyDictionary>( - new Dictionary> - { - // for now, all numbers are padded to 8 char.s - // This will allow a single method to auto-pad numbers. The method will match these as well as date: yyyymmdd - [nameof(Book.LengthInMinutes)] = lb => lb.Book.LengthInMinutes.ToLuceneString(), - ["Length"] = lb => lb.Book.LengthInMinutes.ToLuceneString(), - ["Minutes"] = lb => lb.Book.LengthInMinutes.ToLuceneString(), - ["Hours"] = lb => (lb.Book.LengthInMinutes / 60).ToLuceneString(), - - ["ProductRating"] = lb => lb.Book.Rating.OverallRating.ToLuceneString(), - ["Rating"] = lb => lb.Book.Rating.OverallRating.ToLuceneString(), - ["UserRating"] = lb => userOverallRating(lb.Book), - ["MyRating"] = lb => userOverallRating(lb.Book), - - [nameof(LibraryBook.DateAdded)] = lb => lb.DateAdded.ToLuceneString(), - [nameof(Book.DatePublished)] = lb => lb.Book.DatePublished?.ToLuceneString() ?? "", - - ["LastDownload"] = lb => lb.Book.UserDefinedItem.LastDownloaded.ToLuceneString(), - ["LastDownloaded"] = lb => lb.Book.UserDefinedItem.LastDownloaded.ToLuceneString() - } - ); - - internal static ReadOnlyDictionary> boolIndexRules { get; } - = new ReadOnlyDictionary>( - new Dictionary> - { - ["HasDownloads"] = lb => lb.Book.HasPdf(), - ["HasDownload"] = lb => lb.Book.HasPdf(), - ["Downloads"] = lb => lb.Book.HasPdf(), - ["Download"] = lb => lb.Book.HasPdf(), - ["HasPDFs"] = lb => lb.Book.HasPdf(), - ["HasPDF"] = lb => lb.Book.HasPdf(), - ["PDFs"] = lb => lb.Book.HasPdf(), - ["PDF"] = lb => lb.Book.HasPdf(), - - ["IsRated"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating > 0f, - ["Rated"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating > 0f, - - ["IsAuthorNarrated"] = isAuthorNarrated, - ["AuthorNarrated"] = isAuthorNarrated, - - [nameof(Book.IsAbridged)] = lb => lb.Book.IsAbridged, - ["Abridged"] = lb => lb.Book.IsAbridged, - - ["IsLiberated"] = lb => isLiberated(lb.Book), - ["Liberated"] = lb => isLiberated(lb.Book), - ["LiberatedError"] = lb => liberatedError(lb.Book), - - ["Podcast"] = lb => lb.Book.IsEpisodeChild(), - ["Podcasts"] = lb => lb.Book.IsEpisodeChild(), - ["IsPodcast"] = lb => lb.Book.IsEpisodeChild(), - ["Episode"] = lb => lb.Book.IsEpisodeChild(), - ["Episodes"] = lb => lb.Book.IsEpisodeChild(), - ["IsEpisode"] = lb => lb.Book.IsEpisodeChild(), - - ["Absent"] = lb => lb.AbsentFromLastScan, - ["AbsentFromLastScan"] = lb => lb.AbsentFromLastScan, - } - ); - - private static bool isAuthorNarrated(LibraryBook lb) + private static bool isAuthorNarrated(Book book) { - var authors = lb.Book.Authors.Select(a => a.Name).ToArray(); - var narrators = lb.Book.Narrators.Select(a => a.Name).ToArray(); + var authors = book.Authors.Select(a => a.Name).ToArray(); + var narrators = book.Narrators.Select(a => a.Name).ToArray(); return authors.Intersect(narrators).Any(); } - private static string userOverallRating(Book book) => book.UserDefinedItem.Rating.OverallRating.ToLuceneString(); - 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 - private static IEnumerable> allFieldIndexRules { get; } - = new List> - { - idIndexRules[ALL_AUDIBLE_PRODUCT_ID], - stringIndexRules[ALL_TITLE], - stringIndexRules[ALL_AUTHOR_NAMES], - stringIndexRules[ALL_NARRATOR_NAMES], - stringIndexRules[ALL_SERIES_NAMES] - }; - #endregion - - #region get search fields. used for display in help - public static IEnumerable GetSearchIdFields() + // use these common fields in the "all" default search field + public static IndexRuleCollection FieldIndexRules { get; } = new IndexRuleCollection { - foreach (var key in idIndexRules.Keys) - yield return key; - } - - public static IEnumerable GetSearchStringFields() - { - foreach (var key in stringIndexRules.Keys) - yield return key; - } - - public static IEnumerable GetSearchBoolFields() - { - foreach (var key in boolIndexRules.Keys) - yield return key; - } - - public static IEnumerable GetSearchNumberFields() - { - foreach (var key in numberIndexRules.Keys) - yield return key; - } + { FieldType.ID, Book => Book.AudibleProductId.ToLowerInvariant(), nameof(Book.AudibleProductId), "ProductId", "Id", "ASIN" }, + { FieldType.Raw, Book => Book.AudibleProductId, _ID_ }, + { FieldType.String, Book => Book.Title, nameof(Book.Title), "ProductId", "Id", "ASIN" }, + { FieldType.String, Book => Book.AuthorNames(), "AuthorNames", "Author", "Authors" }, + { FieldType.String, Book => Book.NarratorNames(), "NarratorNames", "Narrator", "Narrators" }, + { FieldType.String, Book => Book.Publisher, nameof(Book.Publisher) }, + { FieldType.String, Book => Book.SeriesNames(), "SeriesNames", "Narrator", "Series" }, + { FieldType.String, Book => string.Join(", ", Book.SeriesLink.Select(s => s.Series.AudibleSeriesId)), "SeriesId" }, + { FieldType.String, Book => Book.CategoriesIds() is null ? null : string.Join(", ", Book.CategoriesIds()), nameof(Book.Category), "Categories", "CategoriesId", "CategoryId", "CategoriesNames" }, + { FieldType.String, Book => Book.UserDefinedItem.Tags, TAGS.FirstCharToUpper() }, + { FieldType.String, Book => Book.Locale, "Locale", "Region" }, + { FieldType.String, lb => lb.Account, "Account", "Email" }, + { FieldType.Bool, Book => Book.HasPdf().ToString(), "HasDownloads", "HasDownload", "Downloads" , "Download", "HasPDFs", "HasPDF" , "PDFs", "PDF" }, + { FieldType.Bool, Book => (Book.UserDefinedItem.Rating.OverallRating > 0f).ToString(), "IsRated", "Rated" }, + { FieldType.Bool, Book => isAuthorNarrated(Book).ToString(), "IsAuthorNarrated", "AuthorNarrated" }, + { FieldType.Bool, Book => Book.IsAbridged.ToString(), nameof(Book.IsAbridged), "Abridged" }, + { FieldType.Bool, Book => (Book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated).ToString(), "IsLiberated", "Liberated" }, + { FieldType.Bool, Book => (Book.UserDefinedItem.BookStatus == LiberatedStatus.Error).ToString(), "LiberatedError" }, + { FieldType.Bool, Book => Book.IsEpisodeChild().ToString(), "Podcast", "Podcasts", "IsPodcast", "Episode", "Episodes", "IsEpisode" }, + { FieldType.Bool, lb => lb.AbsentFromLastScan.ToString(), "AbsentFromLastScan", "Absent" }, + // all numbers are padded to 8 char.s + // This will allow a single method to auto-pad numbers. The method will match these as well as date: yyyymmdd + { FieldType.Number, Book => Book.LengthInMinutes.ToLuceneString(), nameof(Book.LengthInMinutes), "Length", "Minutes" }, + { FieldType.Number, Book => (Book.LengthInMinutes / 60).ToLuceneString(), "Hours" }, + { FieldType.Number, Book => Book.Rating.OverallRating.ToLuceneString(), "ProductRating", "Rating" }, + { FieldType.Number, Book => Book.UserDefinedItem.Rating.OverallRating.ToLuceneString(), "UserRating", "MyRating" }, + { FieldType.Number, Book => Book.DatePublished?.ToLuceneString() ?? "", nameof(Book.DatePublished) }, + { FieldType.Number, Book => Book.UserDefinedItem.LastDownloaded.ToLuceneString(), nameof(UserDefinedItem.LastDownloaded), "LastDownload" }, + { FieldType.Number, lb => lb.DateAdded.ToLuceneString(), nameof(LibraryBook.DateAdded) } + }; #endregion #region create and update index @@ -224,35 +104,15 @@ namespace LibationSearchEngine { var doc = new Document(); - // refine with - // http://codeclimber.net.nz/archive/2009/09/10/how-subtext-lucenenet-index-is-structured/ - - // fields are key value pairs and MULTIPLE FIELDS CAN HAVE THE SAME KEY. - // splitting authors and narrators and/or tags into multiple fields could be interesting research. - // it could allow for more advanced searches, or maybe it could break broad searches. - - // all searching should be lowercase - // external callers have the reasonable expectation that product id will be returned CASE SPECIFIC - doc.AddRaw(_ID_, libraryBook.Book.AudibleProductId); - // concat all common fields for the default 'all' field var allConcat = - allFieldIndexRules - .Select(rule => rule(libraryBook)) + FieldIndexRules + .Select(rule => rule.GetValue(libraryBook)) .Aggregate((a, b) => $"{a} {b}"); doc.AddAnalyzed(ALL, allConcat); - foreach (var kvp in idIndexRules) - doc.AddNotAnalyzed(kvp.Key, kvp.Value(libraryBook)); - - foreach (var kvp in stringIndexRules) - doc.AddAnalyzed(kvp.Key, kvp.Value(libraryBook)); - - foreach (var kvp in boolIndexRules) - doc.AddBool(kvp.Key, kvp.Value(libraryBook)); - - foreach (var kvp in numberIndexRules) - doc.AddNotAnalyzed(kvp.Key, kvp.Value(libraryBook)); + foreach (var rule in FieldIndexRules) + doc.AddIndexRule(rule, libraryBook); return doc; } @@ -267,58 +127,39 @@ namespace LibationSearchEngine productId, d => { - // fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY. - // ie: must remove old before adding new else will create unwanted duplicates. - d.RemoveField(fieldName.ToLower()); + d.RemoveField(fieldName.ToLower()); d.AddAnalyzed(fieldName, newValue); - }); + }); - // update single document entry + // update single document entry public void UpdateLiberatedStatus(Book book) => updateDocument( book.AudibleProductId, d => { - // - // TODO: better synonym handling. This is too easy to mess up - // + var lib = FieldIndexRules.GetRuleByFieldName("IsLiberated"); + var libError = FieldIndexRules.GetRuleByFieldName("LiberatedError"); + var lastDl = FieldIndexRules.GetRuleByFieldName(nameof(UserDefinedItem.LastDownloaded)); - // fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY. - // ie: must remove old before adding new else will create unwanted duplicates. - var v1 = isLiberated(book); - d.RemoveField("isliberated"); - d.AddBool("IsLiberated", v1); - d.RemoveField("liberated"); - d.AddBool("Liberated", v1); + d.RemoveRule(lib); + d.RemoveRule(libError); + d.RemoveRule(lastDl); - var v2 = liberatedError(book); - d.RemoveField("liberatederror"); - d.AddBool("LiberatedError", v2); - - var v3 = book.UserDefinedItem.LastDownloaded?.ToLuceneString() ?? ""; - d.RemoveField("LastDownload"); - d.AddNotAnalyzed("LastDownload", v3); - d.RemoveField("LastDownloaded"); - d.AddNotAnalyzed("LastDownloaded", v3); + d.AddIndexRule(lib, book); + d.AddIndexRule(libError, book); + d.AddIndexRule(lastDl, book); }); public void UpdateUserRatings(Book book) =>updateDocument( book.AudibleProductId, d => - { - // - // TODO: better synonym handling. This is too easy to mess up - // + { + var rating = FieldIndexRules.GetRuleByFieldName("UserRating"); - // fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY. - // ie: must remove old before adding new else will create unwanted duplicates. - var v1 = userOverallRating(book); - d.RemoveField("userrating"); - d.AddNotAnalyzed("UserRating", v1); - d.RemoveField("myrating"); - d.AddNotAnalyzed("MyRating", v1); - }); + d.RemoveRule(rating); + d.AddIndexRule(rating, book); + }); private static void updateDocument(string productId, Action action) { @@ -335,11 +176,9 @@ namespace LibationSearchEngine return; var document = searcher.Doc(scoreDoc.Doc); - // perform update action(document); - // update index var createNewIndex = false; using var analyzer = new StandardAnalyzer(Version); @@ -412,24 +251,24 @@ namespace LibationSearchEngine return returnList; } - private void displayResults(SearchResultSet docs) - { - //for (int i = 0; i < docs.Docs.Count(); i++) - //{ - // var sde = docs.Docs.First(); + private void displayResults(SearchResultSet docs) + { + //for (int i = 0; i < docs.Docs.Count(); i++) + //{ + // var sde = docs.Docs.First(); - // Document doc = sde.Doc; - // float score = sde.Score; + // Document doc = sde.Doc; + // float score = sde.Score; - // Serilog.Log.Logger.Debug($"{(i + 1)}) score={score}. Fields:"); - // var allFields = doc.GetFields(); - // foreach (var f in allFields) - // Serilog.Log.Logger.Debug($" [{f.Name}]={f.StringValue}"); - //} - } - #endregion + // Serilog.Log.Logger.Debug($"{(i + 1)}) score={score}. Fields:"); + // var allFields = doc.GetFields(); + // foreach (var f in allFields) + // Serilog.Log.Logger.Debug($" [{f.Name}]={f.StringValue}"); + //} + } + #endregion - private static Directory getIndex() => FSDirectory.Open(SearchEngineDirectory); + private static Directory getIndex() => FSDirectory.Open(SearchEngineDirectory); // not customizable. don't move to config private static string SearchEngineDirectory { get; } diff --git a/Source/LibationWinForms/Dialogs/SearchSyntaxDialog.cs b/Source/LibationWinForms/Dialogs/SearchSyntaxDialog.cs index d809f33f..f255f520 100644 --- a/Source/LibationWinForms/Dialogs/SearchSyntaxDialog.cs +++ b/Source/LibationWinForms/Dialogs/SearchSyntaxDialog.cs @@ -1,4 +1,5 @@ -using System; +using LibationSearchEngine; +using System; using System.Linq; using System.Windows.Forms; @@ -10,10 +11,10 @@ namespace LibationWinForms.Dialogs { InitializeComponent(); - label2.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchStringFields()); - label3.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchNumberFields()); - label4.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchBoolFields()); - label5.Text += "\r\n\r\n" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchIdFields()); + label2.Text += "\r\n\r\n" + string.Join("\r\n", SearchEngine.FieldIndexRules.StringFieldNames); + label3.Text += "\r\n\r\n" + string.Join("\r\n", SearchEngine.FieldIndexRules.NumberFieldNames); + label4.Text += "\r\n\r\n" + string.Join("\r\n", SearchEngine.FieldIndexRules.BoolFieldNames); + label5.Text += "\r\n\r\n" + string.Join("\r\n", SearchEngine.FieldIndexRules.IdFieldNames); this.SetLibationIcon(); } From 2c4705de6eb930061c316a2df1eb071ce2d450cc Mon Sep 17 00:00:00 2001 From: Mbucari Date: Tue, 13 Jun 2023 08:54:00 -0600 Subject: [PATCH 2/2] Address #625 comments and refactor --- .../BulkSetDownloadStatus.cs | 14 ++-- Source/ApplicationServices/LibraryCommands.cs | 52 ++++++--------- .../SearchEngineCommands.cs | 6 +- Source/FileLiberator/DownloadDecryptBook.cs | 2 +- Source/FileLiberator/DownloadPdf.cs | 2 +- .../Dialogs/BookDetailsDialog.axaml.cs | 2 +- .../ViewModels/ProcessBookViewModel.cs | 2 +- .../ViewModels/ProcessQueueViewModel.cs | 2 +- .../Views/ProductsDisplay.axaml.cs | 4 +- Source/LibationSearchEngine/BookRule.cs | 21 ------ Source/LibationSearchEngine/IIndexRule.cs | 22 ------- Source/LibationSearchEngine/IndexRule.cs | 42 ++++++++++++ .../IndexRuleCollection.cs | 17 +++-- .../LibationSearchEngine/LibraryBookRule.cs | 21 ------ .../LibationSearchEngine/LuceneExtensions.cs | 15 +---- Source/LibationSearchEngine/SearchEngine.cs | 64 +++++++++---------- .../GridView/GridEntry[TStatus].cs | 2 +- .../GridView/ProductsDisplay.cs | 6 +- .../ProcessQueue/ProcessBook.cs | 2 +- .../ProcessQueue/ProcessQueueControl.cs | 2 +- 20 files changed, 126 insertions(+), 174 deletions(-) delete mode 100644 Source/LibationSearchEngine/BookRule.cs delete mode 100644 Source/LibationSearchEngine/IIndexRule.cs create mode 100644 Source/LibationSearchEngine/IndexRule.cs delete mode 100644 Source/LibationSearchEngine/LibraryBookRule.cs diff --git a/Source/ApplicationServices/BulkSetDownloadStatus.cs b/Source/ApplicationServices/BulkSetDownloadStatus.cs index f0f835d3..13a303be 100644 --- a/Source/ApplicationServices/BulkSetDownloadStatus.cs +++ b/Source/ApplicationServices/BulkSetDownloadStatus.cs @@ -10,7 +10,7 @@ namespace ApplicationServices { public class BulkSetDownloadStatus { - private List<(string message, LiberatedStatus newStatus, IEnumerable Books)> actionSets { get; } = new(); + private List<(string message, LiberatedStatus newStatus, IEnumerable LibraryBooks)> actionSets { get; } = new(); public int Count => actionSets.Count; @@ -33,7 +33,7 @@ namespace ApplicationServices var bookExistsList = _libraryBooks .Select(libraryBook => new { - libraryBook.Book, + LibraryBook = libraryBook, FileExists = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId) is not null }) .ToList(); @@ -41,8 +41,8 @@ namespace ApplicationServices if (_setDownloaded) { var books2change = bookExistsList - .Where(a => a.FileExists && a.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated) - .Select(a => a.Book) + .Where(a => a.FileExists && a.LibraryBook.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated) + .Select(a => a.LibraryBook) .ToList(); if (books2change.Any()) @@ -55,8 +55,8 @@ namespace ApplicationServices if (_setNotDownloaded) { var books2change = bookExistsList - .Where(a => !a.FileExists && a.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated) - .Select(a => a.Book) + .Where(a => !a.FileExists && a.LibraryBook.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated) + .Select(a => a.LibraryBook) .ToList(); if (books2change.Any()) @@ -72,7 +72,7 @@ namespace ApplicationServices public void Execute() { foreach (var a in actionSets) - a.Books.UpdateBookStatus(a.newStatus); + a.LibraryBooks.UpdateBookStatus(a.newStatus); } } } diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs index a690458d..5179aec2 100644 --- a/Source/ApplicationServices/LibraryCommands.cs +++ b/Source/ApplicationServices/LibraryCommands.cs @@ -446,25 +446,25 @@ namespace ApplicationServices /// /// Occurs when the size of the library does not change but book(s) details do. Especially when , , or changed values are successfully persisted. /// - public static event EventHandler> BookUserDefinedItemCommitted; + public static event EventHandler> BookUserDefinedItemCommitted; #region Update book details public static int UpdateUserDefinedItem( - this Book book, + this LibraryBook lb, string tags = null, LiberatedStatus? bookStatus = null, LiberatedStatus? pdfStatus = null, Rating rating = null) - => new[] { book }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus, rating); + => new[] { lb }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus, rating); public static int UpdateUserDefinedItem( - this IEnumerable books, + this IEnumerable lb, string tags = null, LiberatedStatus? bookStatus = null, LiberatedStatus? pdfStatus = null, Rating rating = null) => updateUserDefinedItem( - books, + lb, udi => { // blank tags are expected. null tags are not if (tags is not null) @@ -480,66 +480,52 @@ namespace ApplicationServices udi.UpdateRating(rating.OverallRating, rating.PerformanceRating, rating.StoryRating); }); - public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus, Version libationVersion) - => book.UpdateUserDefinedItem(udi => { udi.BookStatus = bookStatus; udi.SetLastDownloaded(libationVersion); }); - public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus) - => book.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus); - public static int UpdateBookStatus(this IEnumerable books, LiberatedStatus bookStatus) - => books.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus); + public static int UpdateBookStatus(this LibraryBook lb, LiberatedStatus bookStatus, Version libationVersion) + => lb.UpdateUserDefinedItem(udi => { udi.BookStatus = bookStatus; udi.SetLastDownloaded(libationVersion); }); + public static int UpdateBookStatus(this LibraryBook libraryBook, LiberatedStatus bookStatus) => libraryBook.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus); public static int UpdateBookStatus(this IEnumerable libraryBooks, LiberatedStatus bookStatus) => libraryBooks.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus); - public static int UpdatePdfStatus(this Book book, LiberatedStatus pdfStatus) - => book.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus)); - public static int UpdatePdfStatus(this IEnumerable books, LiberatedStatus pdfStatus) - => books.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus)); public static int UpdatePdfStatus(this LibraryBook libraryBook, LiberatedStatus pdfStatus) => libraryBook.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus)); public static int UpdatePdfStatus(this IEnumerable libraryBooks, LiberatedStatus pdfStatus) => libraryBooks.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus)); - public static int UpdateTags(this Book book, string tags) - => book.UpdateUserDefinedItem(udi => udi.Tags = tags); - public static int UpdateTags(this IEnumerable books, string tags) - => books.UpdateUserDefinedItem(udi => udi.Tags = tags); public static int UpdateTags(this LibraryBook libraryBook, string tags) => libraryBook.UpdateUserDefinedItem(udi => udi.Tags = tags); public static int UpdateTags(this IEnumerable libraryBooks, string tags) => libraryBooks.UpdateUserDefinedItem(udi => udi.Tags = tags); public static int UpdateUserDefinedItem(this LibraryBook libraryBook, Action action) - => libraryBook.Book.updateUserDefinedItem(action); + => libraryBook.updateUserDefinedItem(action); public static int UpdateUserDefinedItem(this IEnumerable libraryBooks, Action action) - => libraryBooks.Select(lb => lb.Book).updateUserDefinedItem(action); + => libraryBooks.updateUserDefinedItem(action); - public static int UpdateUserDefinedItem(this Book book, Action action) => book.updateUserDefinedItem(action); - public static int UpdateUserDefinedItem(this IEnumerable books, Action action) => books.updateUserDefinedItem(action); - - private static int updateUserDefinedItem(this Book book, Action action) => new[] { book }.updateUserDefinedItem(action); - private static int updateUserDefinedItem(this IEnumerable books, Action action) + private static int updateUserDefinedItem(this LibraryBook libraryBook, Action action) => new[] { libraryBook }.updateUserDefinedItem(action); + private static int updateUserDefinedItem(this IEnumerable libraryBooks, Action action) { try { - if (books is null || !books.Any()) + if (libraryBooks is null || !libraryBooks.Any()) return 0; - foreach (var book in books) - action?.Invoke(book.UserDefinedItem); + foreach (var book in libraryBooks) + action?.Invoke(book.Book.UserDefinedItem); using var context = DbContexts.GetContext(); // Attach() NoTracking entities before SaveChanges() - foreach (var book in books) + foreach (var book in libraryBooks) { - context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified; - context.Attach(book.UserDefinedItem.Rating).State = Microsoft.EntityFrameworkCore.EntityState.Modified; + context.Attach(book.Book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified; + context.Attach(book.Book.UserDefinedItem.Rating).State = Microsoft.EntityFrameworkCore.EntityState.Modified; } var qtyChanges = context.SaveChanges(); if (qtyChanges > 0) - BookUserDefinedItemCommitted?.Invoke(null, books); + BookUserDefinedItemCommitted?.Invoke(null, libraryBooks); return qtyChanges; } diff --git a/Source/ApplicationServices/SearchEngineCommands.cs b/Source/ApplicationServices/SearchEngineCommands.cs index b87326f8..377248d5 100644 --- a/Source/ApplicationServices/SearchEngineCommands.cs +++ b/Source/ApplicationServices/SearchEngineCommands.cs @@ -34,7 +34,7 @@ namespace ApplicationServices #region Update private static bool isUpdating; - public static void UpdateBooks(IEnumerable books) + public static void UpdateBooks(IEnumerable books) { // Semi-arbitrary. At some point it's more worth it to do a full re-index than to do one offs. // I did not benchmark before choosing the number here @@ -49,10 +49,10 @@ namespace ApplicationServices public static void FullReIndex() => performSafeCommand(fullReIndex); - internal static void UpdateUserDefinedItems(Book book) => performSafeCommand(e => + internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e => { e.UpdateLiberatedStatus(book); - e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags); + e.UpdateTags(book.Book.AudibleProductId, book.Book.UserDefinedItem.Tags); e.UpdateUserRatings(book); } ); diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index 3d43c8b6..0318adcf 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -80,7 +80,7 @@ namespace FileLiberator { Task.Run(() => downloadCoverArt(libraryBook)), Task.Run(() => moveFilesToBooksDir(libraryBook, entries)), - Task.Run(() => libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion)), + Task.Run(() => libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion)), Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir)) }; diff --git a/Source/FileLiberator/DownloadPdf.cs b/Source/FileLiberator/DownloadPdf.cs index 9f259cc4..8fe19a12 100644 --- a/Source/FileLiberator/DownloadPdf.cs +++ b/Source/FileLiberator/DownloadPdf.cs @@ -30,7 +30,7 @@ namespace FileLiberator var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath); var result = verifyDownload(actualDownloadedFilePath); - libraryBook.Book.UpdatePdfStatus(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated); + libraryBook.UpdatePdfStatus(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated); return result; } diff --git a/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs index 2cb4218b..0e26b28f 100644 --- a/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml.cs @@ -48,7 +48,7 @@ namespace LibationAvalonia.Dialogs protected override void SaveAndClose() { - LibraryBook.Book.UpdateUserDefinedItem(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus); + LibraryBook.UpdateUserDefinedItem(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus); base.SaveAndClose(); } diff --git a/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs b/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs index d4607ed0..f8fc4159 100644 --- a/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs +++ b/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs @@ -390,7 +390,7 @@ $@" Title: {libraryBook.Book.Title} if (dialogResult == SkipResult) { - libraryBook.Book.UpdateBookStatus(LiberatedStatus.Error); + libraryBook.UpdateBookStatus(LiberatedStatus.Error); Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}"); diff --git a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs b/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs index 50e9cea1..0f86d4b5 100644 --- a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs +++ b/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs @@ -223,7 +223,7 @@ namespace LibationAvalonia.ViewModels else if (result == ProcessBookResult.FailedAbort) Queue.ClearQueue(); else if (result == ProcessBookResult.FailedSkip) - nextBook.LibraryBook.Book.UpdateBookStatus(LiberatedStatus.Error); + nextBook.LibraryBook.UpdateBookStatus(LiberatedStatus.Error); else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage) { await MessageBox.Show(@$" diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs index bd85f92f..2e421f09 100644 --- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs +++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs @@ -112,7 +112,7 @@ namespace LibationAvalonia.Views if (entry.Liberate.IsSeries) setDownloadMenuItem.Click += (_, __) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.Liberated); else - setDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.Liberated); + setDownloadMenuItem.Click += (_, __) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.Liberated); #endregion #region Set Download status to Not Downloaded @@ -128,7 +128,7 @@ namespace LibationAvalonia.Views if (entry.Liberate.IsSeries) setNotDownloadMenuItem.Click += (_, __) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.NotLiberated); else - setNotDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated); + setNotDownloadMenuItem.Click += (_, __) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.NotLiberated); #endregion #region Remove from library diff --git a/Source/LibationSearchEngine/BookRule.cs b/Source/LibationSearchEngine/BookRule.cs deleted file mode 100644 index e7c296e1..00000000 --- a/Source/LibationSearchEngine/BookRule.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DataLayer; -using System; -using System.Collections.ObjectModel; - -namespace LibationSearchEngine; - -public class BookRule : IIndexRule -{ - public FieldType FieldType { get; } - public Func ValueGetter { get; } - public ReadOnlyCollection FieldNames { get; } - - public BookRule(FieldType fieldType, Func valueGetter, params string[] fieldNames) - { - ValueGetter = valueGetter; - FieldType = fieldType; - FieldNames = new ReadOnlyCollection(fieldNames); - } - - public string GetValue(LibraryBook libraryBook) => ValueGetter(libraryBook.Book); -} diff --git a/Source/LibationSearchEngine/IIndexRule.cs b/Source/LibationSearchEngine/IIndexRule.cs deleted file mode 100644 index dcaa441f..00000000 --- a/Source/LibationSearchEngine/IIndexRule.cs +++ /dev/null @@ -1,22 +0,0 @@ -using DataLayer; -using System.Collections.ObjectModel; - -namespace LibationSearchEngine; - -public enum FieldType -{ - Bool, - String, - Number, - ID, - Raw -} - -public interface IIndexRule -{ - /// This rule's value type. - FieldType FieldType { get; } - /// All aliases of this search index rule - ReadOnlyCollection FieldNames { get; } - string GetValue(LibraryBook libraryBook); -} diff --git a/Source/LibationSearchEngine/IndexRule.cs b/Source/LibationSearchEngine/IndexRule.cs new file mode 100644 index 00000000..79ff21cc --- /dev/null +++ b/Source/LibationSearchEngine/IndexRule.cs @@ -0,0 +1,42 @@ +using DataLayer; +using Dinah.Core; +using System; +using System.Collections.ObjectModel; +using System.Linq; + +namespace LibationSearchEngine; + +public enum FieldType +{ + Bool, + String, + Number, + ID, + Raw +} + +public class IndexRule +{ + public FieldType FieldType { get; } + public Func GetValue { get; } + public ReadOnlyCollection FieldNames { get; } + + public IndexRule(FieldType fieldType, Func valueGetter, params string[] fieldNames) + { + ArgumentValidator.EnsureNotNull(valueGetter, nameof(valueGetter)); + ArgumentValidator.EnsureNotNull(fieldNames, nameof(fieldNames)); + ArgumentValidator.EnsureGreaterThan(fieldNames.Length, $"{nameof(fieldNames)}.{nameof(fieldNames.Length)}", 0); + var fieldNamesValidated + = fieldNames + .Select((n, i) => ArgumentValidator.EnsureNotNullOrWhiteSpace(n, $"{nameof(fieldNames)}[{i}]") + .Trim()); + + GetValue = valueGetter; + FieldType = fieldType; + FieldNames = new ReadOnlyCollection(fieldNamesValidated.ToList()); + } + public override string ToString() + => FieldNames.Count == 1 + ? $"{FieldNames.First()}" + : $"{FieldNames.First()} ({string.Join(", ", FieldNames.Skip(1))})"; +} diff --git a/Source/LibationSearchEngine/IndexRuleCollection.cs b/Source/LibationSearchEngine/IndexRuleCollection.cs index 2a20dad1..5033897f 100644 --- a/Source/LibationSearchEngine/IndexRuleCollection.cs +++ b/Source/LibationSearchEngine/IndexRuleCollection.cs @@ -2,27 +2,26 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace LibationSearchEngine; -public class IndexRuleCollection : IEnumerable +[DebuggerDisplay("Count = {rules.Count,nq}")] +public class IndexRuleCollection : IEnumerable { - private readonly List rules = new(); + private readonly List rules = new(); public IEnumerable IdFieldNames => rules.Where(x => x.FieldType is FieldType.ID).SelectMany(r => r.FieldNames); public IEnumerable BoolFieldNames => rules.Where(x => x.FieldType is FieldType.Bool).SelectMany(r => r.FieldNames); public IEnumerable StringFieldNames => rules.Where(x => x.FieldType is FieldType.String).SelectMany(r => r.FieldNames); public IEnumerable NumberFieldNames => rules.Where(x => x.FieldType is FieldType.Number).SelectMany(r => r.FieldNames); public void Add(FieldType fieldType, Func getter, params string[] fieldNames) - => rules.Add(new LibraryBookRule(fieldType, getter, fieldNames)); - - public void Add(FieldType fieldType, Func getter, params string[] fieldNames) - => rules.Add(new BookRule(fieldType, getter, fieldNames)); + => rules.Add(new IndexRule(fieldType, getter, fieldNames)); - public T GetRuleByFieldName(string fieldName) where T : IIndexRule - => (T)rules.SingleOrDefault(r => r.FieldNames.Any(n => n.Equals(fieldName, StringComparison.OrdinalIgnoreCase))); + public IndexRule GetRuleByFieldName(string fieldName) + => rules.SingleOrDefault(r => r.FieldNames.Any(n => n.Equals(fieldName, StringComparison.OrdinalIgnoreCase))); - public IEnumerator GetEnumerator() => rules.GetEnumerator(); + public IEnumerator GetEnumerator() => rules.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/Source/LibationSearchEngine/LibraryBookRule.cs b/Source/LibationSearchEngine/LibraryBookRule.cs deleted file mode 100644 index 6c9036b7..00000000 --- a/Source/LibationSearchEngine/LibraryBookRule.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DataLayer; -using System; -using System.Collections.ObjectModel; - -namespace LibationSearchEngine; - -public class LibraryBookRule : IIndexRule -{ - public FieldType FieldType { get; } - public Func ValueGetter { get; } - public ReadOnlyCollection FieldNames { get; } - - public LibraryBookRule(FieldType fieldType, Func valueGetter, params string[] fieldNames) - { - ValueGetter = valueGetter; - FieldType = fieldType; - FieldNames = new ReadOnlyCollection(fieldNames); - } - - public string GetValue(LibraryBook libraryBook) => ValueGetter(libraryBook); -} diff --git a/Source/LibationSearchEngine/LuceneExtensions.cs b/Source/LibationSearchEngine/LuceneExtensions.cs index 0e09fe8d..32e7ab46 100644 --- a/Source/LibationSearchEngine/LuceneExtensions.cs +++ b/Source/LibationSearchEngine/LuceneExtensions.cs @@ -17,7 +17,7 @@ namespace LibationSearchEngine document.Add(new Field(name.ToLowerInvariant(), value, Field.Store.YES, Field.Index.ANALYZED)); } - internal static void RemoveRule(this Document document, IIndexRule rule) + internal static void RemoveRule(this Document document, IndexRule rule) { // fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY. // ie: must remove old before adding new else will create unwanted duplicates. @@ -25,20 +25,9 @@ namespace LibationSearchEngine document.RemoveFields(name.ToLowerInvariant()); } - internal static void AddIndexRule(this Document document, IIndexRule rule, LibraryBook libraryBook) + internal static void AddIndexRule(this Document document, IndexRule rule, LibraryBook libraryBook) { string value = rule.GetValue(libraryBook); - - addIndexRule(document, rule, value); - } - - internal static void AddIndexRule(this Document document, BookRule rule, Book libraryBook) - { - addIndexRule(document, rule, rule.ValueGetter(libraryBook)); - } - - private static void addIndexRule(Document document, IIndexRule rule, string value) - { if (value is null) return; foreach (var name in rule.FieldNames) diff --git a/Source/LibationSearchEngine/SearchEngine.cs b/Source/LibationSearchEngine/SearchEngine.cs index 872a4b1a..00b80db7 100644 --- a/Source/LibationSearchEngine/SearchEngine.cs +++ b/Source/LibationSearchEngine/SearchEngine.cs @@ -33,34 +33,34 @@ namespace LibationSearchEngine // use these common fields in the "all" default search field public static IndexRuleCollection FieldIndexRules { get; } = new IndexRuleCollection { - { FieldType.ID, Book => Book.AudibleProductId.ToLowerInvariant(), nameof(Book.AudibleProductId), "ProductId", "Id", "ASIN" }, - { FieldType.Raw, Book => Book.AudibleProductId, _ID_ }, - { FieldType.String, Book => Book.Title, nameof(Book.Title), "ProductId", "Id", "ASIN" }, - { FieldType.String, Book => Book.AuthorNames(), "AuthorNames", "Author", "Authors" }, - { FieldType.String, Book => Book.NarratorNames(), "NarratorNames", "Narrator", "Narrators" }, - { FieldType.String, Book => Book.Publisher, nameof(Book.Publisher) }, - { FieldType.String, Book => Book.SeriesNames(), "SeriesNames", "Narrator", "Series" }, - { FieldType.String, Book => string.Join(", ", Book.SeriesLink.Select(s => s.Series.AudibleSeriesId)), "SeriesId" }, - { FieldType.String, Book => Book.CategoriesIds() is null ? null : string.Join(", ", Book.CategoriesIds()), nameof(Book.Category), "Categories", "CategoriesId", "CategoryId", "CategoriesNames" }, - { FieldType.String, Book => Book.UserDefinedItem.Tags, TAGS.FirstCharToUpper() }, - { FieldType.String, Book => Book.Locale, "Locale", "Region" }, + { FieldType.ID, lb => lb.Book.AudibleProductId.ToLowerInvariant(), nameof(Book.AudibleProductId), "ProductId", "Id", "ASIN" }, + { FieldType.Raw, lb => lb.Book.AudibleProductId, _ID_ }, + { FieldType.String, lb => lb.Book.Title, nameof(Book.Title), "ProductId", "Id", "ASIN" }, + { FieldType.String, lb => lb.Book.AuthorNames(), "AuthorNames", "Author", "Authors" }, + { FieldType.String, lb => lb.Book.NarratorNames(), "NarratorNames", "Narrator", "Narrators" }, + { FieldType.String, lb => lb.Book.Publisher, nameof(Book.Publisher) }, + { FieldType.String, lb => lb.Book.SeriesNames(), "SeriesNames", "Narrator", "Series" }, + { FieldType.String, lb => string.Join(", ", lb.Book.SeriesLink.Select(s => s.Series.AudibleSeriesId)), "SeriesId" }, + { FieldType.String, lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()), nameof(Book.Category), "Categories", "CategoriesId", "CategoryId", "CategoriesNames" }, + { FieldType.String, lb => lb.Book.UserDefinedItem.Tags, TAGS.FirstCharToUpper() }, + { FieldType.String, lb => lb.Book.Locale, "Locale", "Region" }, { FieldType.String, lb => lb.Account, "Account", "Email" }, - { FieldType.Bool, Book => Book.HasPdf().ToString(), "HasDownloads", "HasDownload", "Downloads" , "Download", "HasPDFs", "HasPDF" , "PDFs", "PDF" }, - { FieldType.Bool, Book => (Book.UserDefinedItem.Rating.OverallRating > 0f).ToString(), "IsRated", "Rated" }, - { FieldType.Bool, Book => isAuthorNarrated(Book).ToString(), "IsAuthorNarrated", "AuthorNarrated" }, - { FieldType.Bool, Book => Book.IsAbridged.ToString(), nameof(Book.IsAbridged), "Abridged" }, - { FieldType.Bool, Book => (Book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated).ToString(), "IsLiberated", "Liberated" }, - { FieldType.Bool, Book => (Book.UserDefinedItem.BookStatus == LiberatedStatus.Error).ToString(), "LiberatedError" }, - { FieldType.Bool, Book => Book.IsEpisodeChild().ToString(), "Podcast", "Podcasts", "IsPodcast", "Episode", "Episodes", "IsEpisode" }, + { FieldType.Bool, lb => lb.Book.HasPdf().ToString(), "HasDownloads", "HasDownload", "Downloads" , "Download", "HasPDFs", "HasPDF" , "PDFs", "PDF" }, + { FieldType.Bool, lb => (lb.Book.UserDefinedItem.Rating.OverallRating > 0f).ToString(), "IsRated", "Rated" }, + { FieldType.Bool, lb => isAuthorNarrated(lb.Book).ToString(), "IsAuthorNarrated", "AuthorNarrated" }, + { FieldType.Bool, lb => lb.Book.IsAbridged.ToString(), nameof(Book.IsAbridged), "Abridged" }, + { FieldType.Bool, lb => (lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated).ToString(), "IsLiberated", "Liberated" }, + { FieldType.Bool, lb => (lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.Error).ToString(), "LiberatedError" }, + { FieldType.Bool, lb => lb.Book.IsEpisodeChild().ToString(), "Podcast", "Podcasts", "IsPodcast", "Episode", "Episodes", "IsEpisode" }, { FieldType.Bool, lb => lb.AbsentFromLastScan.ToString(), "AbsentFromLastScan", "Absent" }, // all numbers are padded to 8 char.s // This will allow a single method to auto-pad numbers. The method will match these as well as date: yyyymmdd - { FieldType.Number, Book => Book.LengthInMinutes.ToLuceneString(), nameof(Book.LengthInMinutes), "Length", "Minutes" }, - { FieldType.Number, Book => (Book.LengthInMinutes / 60).ToLuceneString(), "Hours" }, - { FieldType.Number, Book => Book.Rating.OverallRating.ToLuceneString(), "ProductRating", "Rating" }, - { FieldType.Number, Book => Book.UserDefinedItem.Rating.OverallRating.ToLuceneString(), "UserRating", "MyRating" }, - { FieldType.Number, Book => Book.DatePublished?.ToLuceneString() ?? "", nameof(Book.DatePublished) }, - { FieldType.Number, Book => Book.UserDefinedItem.LastDownloaded.ToLuceneString(), nameof(UserDefinedItem.LastDownloaded), "LastDownload" }, + { FieldType.Number, lb => lb.Book.LengthInMinutes.ToLuceneString(), nameof(Book.LengthInMinutes), "Length", "Minutes" }, + { FieldType.Number, lb => (lb.Book.LengthInMinutes / 60).ToLuceneString(), "Hours" }, + { FieldType.Number, lb => lb.Book.Rating.OverallRating.ToLuceneString(), "ProductRating", "Rating" }, + { FieldType.Number, lb => lb.Book.UserDefinedItem.Rating.OverallRating.ToLuceneString(), "UserRating", "MyRating" }, + { FieldType.Number, lb => lb.Book.DatePublished?.ToLuceneString() ?? "", nameof(Book.DatePublished) }, + { FieldType.Number, lb => lb.Book.UserDefinedItem.LastDownloaded.ToLuceneString(), nameof(UserDefinedItem.LastDownloaded), "LastDownload" }, { FieldType.Number, lb => lb.DateAdded.ToLuceneString(), nameof(LibraryBook.DateAdded) } }; #endregion @@ -132,14 +132,14 @@ namespace LibationSearchEngine }); // update single document entry - public void UpdateLiberatedStatus(Book book) + public void UpdateLiberatedStatus(LibraryBook book) => updateDocument( - book.AudibleProductId, + book.Book.AudibleProductId, d => { - var lib = FieldIndexRules.GetRuleByFieldName("IsLiberated"); - var libError = FieldIndexRules.GetRuleByFieldName("LiberatedError"); - var lastDl = FieldIndexRules.GetRuleByFieldName(nameof(UserDefinedItem.LastDownloaded)); + var lib = FieldIndexRules.GetRuleByFieldName("IsLiberated"); + var libError = FieldIndexRules.GetRuleByFieldName("LiberatedError"); + var lastDl = FieldIndexRules.GetRuleByFieldName(nameof(UserDefinedItem.LastDownloaded)); d.RemoveRule(lib); d.RemoveRule(libError); @@ -150,12 +150,12 @@ namespace LibationSearchEngine d.AddIndexRule(lastDl, book); }); - public void UpdateUserRatings(Book book) + public void UpdateUserRatings(LibraryBook book) =>updateDocument( - book.AudibleProductId, + book.Book.AudibleProductId, d => { - var rating = FieldIndexRules.GetRuleByFieldName("UserRating"); + var rating = FieldIndexRules.GetRuleByFieldName("UserRating"); d.RemoveRule(rating); d.AddIndexRule(rating, book); diff --git a/Source/LibationUiBase/GridView/GridEntry[TStatus].cs b/Source/LibationUiBase/GridView/GridEntry[TStatus].cs index 52513304..01d89f01 100644 --- a/Source/LibationUiBase/GridView/GridEntry[TStatus].cs +++ b/Source/LibationUiBase/GridView/GridEntry[TStatus].cs @@ -88,7 +88,7 @@ namespace LibationUiBase.GridView var api = await LibraryBook.GetApiAsync(); if (await api.ReviewAsync(Book.AudibleProductId, (int)rating.OverallRating, (int)rating.PerformanceRating, (int)rating.StoryRating)) - LibraryBook.Book.UpdateUserDefinedItem(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, rating); + LibraryBook.UpdateUserDefinedItem(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, rating); } #endregion diff --git a/Source/LibationWinForms/GridView/ProductsDisplay.cs b/Source/LibationWinForms/GridView/ProductsDisplay.cs index 95c1ea7f..342c5fcf 100644 --- a/Source/LibationWinForms/GridView/ProductsDisplay.cs +++ b/Source/LibationWinForms/GridView/ProductsDisplay.cs @@ -93,7 +93,7 @@ namespace LibationWinForms.GridView { var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook); if (bookDetailsForm.ShowDialog() == DialogResult.OK) - liveGridEntry.Book.UpdateUserDefinedItem(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus); + liveGridEntry.LibraryBook.UpdateUserDefinedItem(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus); } #endregion @@ -131,7 +131,7 @@ namespace LibationWinForms.GridView if (entry.Liberate.IsSeries) setDownloadMenuItem.Click += (_, _) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.Liberated); else - setDownloadMenuItem.Click += (_, _) => entry.Book.UpdateBookStatus(LiberatedStatus.Liberated); + setDownloadMenuItem.Click += (_, _) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.Liberated); #endregion #region Set Download status to Not Downloaded @@ -147,7 +147,7 @@ namespace LibationWinForms.GridView if (entry.Liberate.IsSeries) setNotDownloadMenuItem.Click += (_, _) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.NotLiberated); else - setNotDownloadMenuItem.Click += (_, _) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated); + setNotDownloadMenuItem.Click += (_, _) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.NotLiberated); #endregion #region Remove from library diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBook.cs b/Source/LibationWinForms/ProcessQueue/ProcessBook.cs index 1159b24b..50b3e1b5 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessBook.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessBook.cs @@ -377,7 +377,7 @@ $@" Title: {libraryBook.Book.Title} if (dialogResult == SkipResult) { - libraryBook.Book.UpdateBookStatus(LiberatedStatus.Error); + libraryBook.UpdateBookStatus(LiberatedStatus.Error); Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}"); diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs index af788041..e05b23b7 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs @@ -184,7 +184,7 @@ namespace LibationWinForms.ProcessQueue else if (result == ProcessBookResult.FailedAbort) Queue.ClearQueue(); else if (result == ProcessBookResult.FailedSkip) - nextBook.LibraryBook.Book.UpdateBookStatus(DataLayer.LiberatedStatus.Error); + nextBook.LibraryBook.UpdateBookStatus(DataLayer.LiberatedStatus.Error); else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage) { MessageBox.Show(@$"