diff --git a/RMuseum/Controllers/GanjoorController.cs b/RMuseum/Controllers/GanjoorController.cs
index f9018351..7af9bfb0 100644
--- a/RMuseum/Controllers/GanjoorController.cs
+++ b/RMuseum/Controllers/GanjoorController.cs
@@ -59,6 +59,50 @@ namespace RMuseum.Controllers
return Ok(poets);
}
+ ///
+ /// get list of books (GanjoorCat entries whose CatType is Book, sorted alphabetically by name),
+ /// optionally filtered by (part of) name and/or poet id. Not to be confused with
+ /// GetBooksAsync()/"books" above, which lists GanjoorCat entries by their (separate,
+ /// legacy) BookName field for cover-image generation.
+ ///
+ /// optional, only books whose name contains this
+ /// optional, only books belonging to this poet
+ ///
+ [HttpGet]
+ [Route("book-catalog")]
+ [AllowAnonymous]
+ [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorBookViewModel[]))]
+ [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
+ public async Task GetBookCatalog(string name = null, int? poetId = null)
+ {
+ // only the common, unfiltered call (the one the home page's book search will use to load
+ // the full list once and filter client-side) is worth caching - a filtered call is cheap
+ // on a table this small (well under 200 rows) and caching every name/poetId combination
+ // would just grow the cache for no benefit.
+ if (string.IsNullOrEmpty(name) && poetId == null)
+ {
+ var cacheKey = $"ganjoor/book-catalog";
+ if (!_memoryCache.TryGetValue(cacheKey, out GanjoorBookViewModel[] cachedBooks))
+ {
+ RServiceResult cachedRes =
+ await _ganjoorService.GetBookCatalogAsync();
+ if (!string.IsNullOrEmpty(cachedRes.ExceptionString))
+ return BadRequest(cachedRes.ExceptionString);
+
+ cachedBooks = cachedRes.Result;
+ if (AggressiveCacheEnabled)
+ _memoryCache.Set(cacheKey, cachedBooks, TimeSpan.FromHours(1));
+ }
+ return Ok(cachedBooks);
+ }
+
+ RServiceResult res =
+ await _ganjoorService.GetBookCatalogAsync(name, poetId);
+ if (!string.IsNullOrEmpty(res.ExceptionString))
+ return BadRequest(res.ExceptionString);
+ return Ok(res.Result);
+ }
+
///
/// gets list of poets grouped by centuries (first one is the pinned ones)
///
diff --git a/RMuseum/Models/Ganjoor/ViewModels/GanjoorBookViewModel.cs b/RMuseum/Models/Ganjoor/ViewModels/GanjoorBookViewModel.cs
new file mode 100644
index 00000000..7504eaa9
--- /dev/null
+++ b/RMuseum/Models/Ganjoor/ViewModels/GanjoorBookViewModel.cs
@@ -0,0 +1,39 @@
+namespace RMuseum.Models.Ganjoor.ViewModels
+{
+ ///
+ /// A GanjoorCat whose CatType is Book - a minimal view model for listing/searching books
+ /// (e.g. the home page's book search box), not the full category details
+ ///
+ public class GanjoorBookViewModel
+ {
+ ///
+ /// id (GanjoorCat.Id)
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// book name (GanjoorCat.Title)
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// full url, sample: /hafez/ghazal
+ ///
+ public string FullUrl { get; set; }
+
+ ///
+ /// poet id (GanjoorCat.PoetId)
+ ///
+ public int PoetId { get; set; }
+
+ ///
+ /// poet short name (GanjoorPoet.Nickname)
+ ///
+ public string PoetName { get; set; }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+}
diff --git a/RMuseum/RMuseum.xml b/RMuseum/RMuseum.xml
index 6d6ea177..f484ad83 100644
--- a/RMuseum/RMuseum.xml
+++ b/RMuseum/RMuseum.xml
@@ -1096,6 +1096,17 @@
+
+
+ get list of books (GanjoorCat entries whose CatType is Book, sorted alphabetically by name),
+ optionally filtered by (part of) name and/or poet id. Not to be confused with
+ GetBooksAsync()/"books" above, which lists GanjoorCat entries by their (separate,
+ legacy) BookName field for cover-image generation.
+
+ optional, only books whose name contains this
+ optional, only books belonging to this poet
+
+
gets list of poets grouped by centuries (first one is the pinned ones)
@@ -12578,6 +12589,37 @@
simulate naming
+
+
+ A GanjoorCat whose CatType is Book - a minimal view model for listing/searching books
+ (e.g. the home page's book search box), not the full category details
+
+
+
+
+ id (GanjoorCat.Id)
+
+
+
+
+ book name (GanjoorCat.Title)
+
+
+
+
+ full url, sample: /hafez/ghazal
+
+
+
+
+ poet id (GanjoorCat.PoetId)
+
+
+
+
+ poet short name (GanjoorPoet.Nickname)
+
+
correct id
@@ -18549,6 +18591,16 @@
+
+
+ Get list of books (GanjoorCat entries whose CatType is Book), sorted alphabetically by name.
+ Not to be confused with GetBooksAsync() below, which lists GanjoorCat entries by their
+ (separate, legacy) BookName field for cover-image generation.
+
+ optional, only books whose name contains this (case-insensitive)
+ optional, only books belonging to this poet
+
+
get poet by id
@@ -22286,6 +22338,9 @@
IGanjoorService implementation
+
+ IGanjoorService implementation
+
@@ -22366,6 +22421,16 @@
+
+
+ Get list of books (GanjoorCat entries whose CatType is Book), sorted alphabetically by name.
+ Not to be confused with GetBooksAsync() elsewhere in this class, which lists GanjoorCat
+ entries by their (separate, legacy) BookName field for cover-image generation.
+
+ optional, only books whose name contains this (case-insensitive)
+ optional, only books belonging to this poet
+
+
category word counts
diff --git a/RMuseum/Services/IGanjoorService.cs b/RMuseum/Services/IGanjoorService.cs
index 39024c18..35c8e68f 100644
--- a/RMuseum/Services/IGanjoorService.cs
+++ b/RMuseum/Services/IGanjoorService.cs
@@ -22,6 +22,16 @@ namespace RMuseum.Services
///
Task> GetPoets(bool published, bool includeBio = true);
+ ///
+ /// Get list of books (GanjoorCat entries whose CatType is Book), sorted alphabetically by name.
+ /// Not to be confused with GetBooksAsync() below, which lists GanjoorCat entries by their
+ /// (separate, legacy) BookName field for cover-image generation.
+ ///
+ /// optional, only books whose name contains this (case-insensitive)
+ /// optional, only books belonging to this poet
+ ///
+ Task> GetBookCatalogAsync(string name = null, int? poetId = null);
+
///
/// get poet by id
///
diff --git a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Books.cs b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Books.cs
new file mode 100644
index 00000000..bbc7cb82
--- /dev/null
+++ b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Books.cs
@@ -0,0 +1,61 @@
+using Microsoft.EntityFrameworkCore;
+using RMuseum.Models.Ganjoor;
+using RMuseum.Models.Ganjoor.ViewModels;
+using RSecurityBackend.Models.Generic;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace RMuseum.Services.Implementation
+{
+ ///
+ /// IGanjoorService implementation
+ ///
+ public partial class GanjoorService : IGanjoorService
+ {
+ ///
+ /// Get list of books (GanjoorCat entries whose CatType is Book), sorted alphabetically by name.
+ /// Not to be confused with GetBooksAsync() elsewhere in this class, which lists GanjoorCat
+ /// entries by their (separate, legacy) BookName field for cover-image generation.
+ ///
+ /// optional, only books whose name contains this (case-insensitive)
+ /// optional, only books belonging to this poet
+ ///
+ public async Task> GetBookCatalogAsync(string name = null, int? poetId = null)
+ {
+ var query = _context.GanjoorCategories.AsNoTracking()
+ .Where(c => c.CatType == GanjoorCatType.Book);
+
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ query = query.Where(c => c.Title.Contains(name));
+ }
+
+ if (poetId != null)
+ {
+ query = query.Where(c => c.PoetId == poetId.Value);
+ }
+
+ List books =
+ await query
+ .Select(c => new GanjoorBookViewModel()
+ {
+ Id = c.Id,
+ Name = c.Title,
+ FullUrl = c.FullUrl,
+ PoetId = c.PoetId,
+ PoetName = c.Poet.Nickname,
+ })
+ .ToListAsync();
+
+ // the fa-IR comparer can't be translated to SQL, so sorting happens here in memory -
+ // the same approach GetPoets() above uses, fine for a list this small (< 200 rows)
+ StringComparer fa = StringComparer.Create(new CultureInfo("fa-IR"), true);
+ books.Sort((a, b) => fa.Compare(a.Name, b.Name));
+
+ return new RServiceResult(books.ToArray());
+ }
+ }
+}