feat: add studio filter to search pipeline
This commit is contained in:
@@ -8,8 +8,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SearchAdvanced performs a filtered anime search with type, status, ordering, and genre filters.
|
||||
func (c *Client) SearchAdvanced(ctx context.Context, query, animeType, status, orderBy, sort string, genres []int, sfw bool, page, limit int) (SearchResult, error) {
|
||||
// SearchAdvanced performs a filtered anime search with type, status, ordering, genre filters, and studio (producer) filters.
|
||||
func (c *Client) SearchAdvanced(ctx context.Context, query, animeType, status, orderBy, sort string, genres []int, studioID int, sfw bool, page, limit int) (SearchResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
@@ -26,7 +26,7 @@ func (c *Client) SearchAdvanced(ctx context.Context, query, animeType, status, o
|
||||
genresParam = strings.Join(ids, ",")
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("search:%s:%s:%s:%s:%s:%s:%v:%d:%d", query, animeType, status, orderBy, sort, genresParam, sfw, page, limit)
|
||||
cacheKey := fmt.Sprintf("search:%s:%s:%s:%s:%s:%s:%d:%v:%d:%d", query, animeType, status, orderBy, sort, genresParam, studioID, sfw, page, limit)
|
||||
|
||||
var result SearchResponse
|
||||
reqURL := fmt.Sprintf("%s/anime?page=%d", c.baseURL, page)
|
||||
@@ -42,6 +42,9 @@ func (c *Client) SearchAdvanced(ctx context.Context, query, animeType, status, o
|
||||
if status != "" {
|
||||
reqURL += "&status=" + url.QueryEscape(status)
|
||||
}
|
||||
if studioID > 0 {
|
||||
reqURL += "&producers=" + strconv.Itoa(studioID)
|
||||
}
|
||||
if orderBy != "" {
|
||||
reqURL += "&order_by=" + url.QueryEscape(orderBy)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"mal/integrations/jikan"
|
||||
"mal/internal/db"
|
||||
"mal/internal/domain"
|
||||
"net/http"
|
||||
@@ -66,6 +67,75 @@ func (h *AnimeHandler) Register(r *gin.Engine) {
|
||||
r.GET("/api/search-quick", h.HandleQuickSearch)
|
||||
r.GET("/api/command-palette", h.HandleCommandPalette)
|
||||
r.GET("/api/jikan/random/anime", h.HandleRandomAnime)
|
||||
r.GET("/api/jikan/producers", h.HandleProducers)
|
||||
}
|
||||
|
||||
func (h *AnimeHandler) HandleProducers(c *gin.Context) {
|
||||
q := strings.TrimSpace(c.Query("q"))
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
|
||||
if limit < 1 {
|
||||
limit = 12
|
||||
}
|
||||
if limit > 12 {
|
||||
limit = 12
|
||||
}
|
||||
|
||||
res, err := h.svc.GetProducers(c.Request.Context(), q, page, limit)
|
||||
if err != nil {
|
||||
log.Printf("failed to fetch producers list (q=%q page=%d limit=%d): %v", q, page, limit, err)
|
||||
if strings.Contains(c.GetHeader("Accept"), "text/html") {
|
||||
c.HTML(http.StatusOK, "browse.gohtml", gin.H{
|
||||
"_fragment": "studio_dropdown_items",
|
||||
"StudioItems": []any{},
|
||||
"HasNextPage": false,
|
||||
"Page": page,
|
||||
"NextPage": page + 1,
|
||||
"Query": q,
|
||||
"Limit": limit,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
type item struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
items := make([]item, 0, len(res.Items))
|
||||
for _, p := range res.Items {
|
||||
name := jikan.ProducerListEntryName(p)
|
||||
if p.MalID <= 0 || name == "" {
|
||||
continue
|
||||
}
|
||||
items = append(items, item{ID: p.MalID, Name: name})
|
||||
}
|
||||
|
||||
if strings.Contains(c.GetHeader("Accept"), "text/html") {
|
||||
c.HTML(http.StatusOK, "browse.gohtml", gin.H{
|
||||
"_fragment": "studio_dropdown_items",
|
||||
"StudioItems": items,
|
||||
"HasNextPage": res.HasNextPage,
|
||||
"Page": page,
|
||||
"NextPage": page + 1,
|
||||
"Query": q,
|
||||
"Limit": limit,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"items": items,
|
||||
"hasNextPage": res.HasNextPage,
|
||||
"nextPage": page + 1,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AnimeHandler) HandleCatalog(c *gin.Context) {
|
||||
@@ -155,6 +225,10 @@ func (h *AnimeHandler) HandleBrowse(c *gin.Context) {
|
||||
orderBy := c.Query("order_by")
|
||||
sort := c.Query("sort")
|
||||
sfw := c.Query("sfw") != "false"
|
||||
studioID, _ := strconv.Atoi(c.Query("studio"))
|
||||
if studioID < 0 {
|
||||
studioID = 0
|
||||
}
|
||||
|
||||
var genres []int
|
||||
for _, g := range c.QueryArray("genres") {
|
||||
@@ -169,7 +243,7 @@ func (h *AnimeHandler) HandleBrowse(c *gin.Context) {
|
||||
page = 1
|
||||
}
|
||||
|
||||
res, err := h.svc.SearchAdvanced(c.Request.Context(), q, animeType, status, orderBy, sort, genres, sfw, page, 24)
|
||||
res, err := h.svc.SearchAdvanced(c.Request.Context(), q, animeType, status, orderBy, sort, genres, studioID, sfw, page, 24)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -182,6 +256,14 @@ func (h *AnimeHandler) HandleBrowse(c *gin.Context) {
|
||||
}
|
||||
watchlistMap := h.watchlistMapForAnimes(c.Request.Context(), userID, res.Animes)
|
||||
|
||||
studioName := ""
|
||||
if studioID > 0 {
|
||||
name, err := h.svc.GetProducerNameByID(c.Request.Context(), studioID)
|
||||
if err == nil {
|
||||
studioName = name
|
||||
}
|
||||
}
|
||||
|
||||
if c.GetHeader("HX-Request") == "true" && page > 1 {
|
||||
c.HTML(http.StatusOK, "browse.gohtml", gin.H{
|
||||
"_fragment": "anime_card_scroll",
|
||||
@@ -194,6 +276,8 @@ func (h *AnimeHandler) HandleBrowse(c *gin.Context) {
|
||||
"OrderBy": orderBy,
|
||||
"Sort": sort,
|
||||
"Genres": genres,
|
||||
"Studio": studioID,
|
||||
"StudioName": studioName,
|
||||
"SFW": sfw,
|
||||
"WatchlistMap": watchlistMap,
|
||||
})
|
||||
@@ -212,6 +296,8 @@ func (h *AnimeHandler) HandleBrowse(c *gin.Context) {
|
||||
"OrderBy": orderBy,
|
||||
"Sort": sort,
|
||||
"Genres": genres,
|
||||
"Studio": studioID,
|
||||
"StudioName": studioName,
|
||||
"SFW": sfw,
|
||||
"GenresList": genresList,
|
||||
"Animes": res.Animes,
|
||||
@@ -231,6 +317,8 @@ func (h *AnimeHandler) HandleBrowse(c *gin.Context) {
|
||||
"OrderBy": orderBy,
|
||||
"Sort": sort,
|
||||
"Genres": genres,
|
||||
"Studio": studioID,
|
||||
"StudioName": studioName,
|
||||
"SFW": sfw,
|
||||
"GenresList": genresList,
|
||||
"Animes": res.Animes,
|
||||
@@ -367,7 +455,7 @@ func (h *AnimeHandler) HandleQuickSearch(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := h.svc.SearchAdvanced(c.Request.Context(), query, "", "", "", "", nil, true, 1, 5)
|
||||
res, err := h.svc.SearchAdvanced(c.Request.Context(), query, "", "", "", "", nil, 0, true, 1, 5)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, []any{})
|
||||
return
|
||||
@@ -475,7 +563,7 @@ func (h *AnimeHandler) commandPaletteAnimeResults(c *gin.Context, query string)
|
||||
searchCtx, cancel := context.WithTimeout(c.Request.Context(), 800*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
res, err := h.svc.SearchAdvanced(searchCtx, query, "", "", "", "", nil, true, 1, 5)
|
||||
res, err := h.svc.SearchAdvanced(searchCtx, query, "", "", "", "", nil, 0, true, 1, 5)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -98,8 +98,25 @@ func (s *animeService) GetAnimeByID(ctx context.Context, id int) (domain.Anime,
|
||||
return s.jikan.GetAnimeByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *animeService) SearchAdvanced(ctx context.Context, q, animeType, status, orderBy, sort string, genres []int, sfw bool, page, limit int) (jikan.SearchResult, error) {
|
||||
return s.jikan.SearchAdvanced(ctx, q, animeType, status, orderBy, sort, genres, sfw, page, limit)
|
||||
func (s *animeService) SearchAdvanced(ctx context.Context, q, animeType, status, orderBy, sort string, genres []int, studioID int, sfw bool, page, limit int) (jikan.SearchResult, error) {
|
||||
return s.jikan.SearchAdvanced(ctx, q, animeType, status, orderBy, sort, genres, studioID, sfw, page, limit)
|
||||
}
|
||||
|
||||
func (s *animeService) GetProducerNameByID(ctx context.Context, id int) (string, error) {
|
||||
res, err := s.jikan.GetProducerByID(ctx, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, t := range res.Data.Titles {
|
||||
if t.Title != "" {
|
||||
return t.Title, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (s *animeService) GetProducers(ctx context.Context, query string, page int, limit int) (jikan.ProducerListResult, error) {
|
||||
return s.jikan.GetProducers(ctx, query, page, limit)
|
||||
}
|
||||
|
||||
func (s *animeService) GetGenres(ctx context.Context) ([]domain.Genre, error) {
|
||||
|
||||
@@ -20,7 +20,9 @@ type AnimeService interface {
|
||||
GetCatalogSection(ctx context.Context, userID string, section string) (CatalogSectionData, error)
|
||||
GetDiscoverSection(ctx context.Context, userID string, section string) (DiscoverSectionData, error)
|
||||
GetAnimeByID(ctx context.Context, id int) (Anime, error)
|
||||
SearchAdvanced(ctx context.Context, q, animeType, status, orderBy, sort string, genres []int, sfw bool, page, limit int) (jikan.SearchResult, error)
|
||||
SearchAdvanced(ctx context.Context, q, animeType, status, orderBy, sort string, genres []int, studioID int, sfw bool, page, limit int) (jikan.SearchResult, error)
|
||||
GetProducerNameByID(ctx context.Context, id int) (string, error)
|
||||
GetProducers(ctx context.Context, query string, page int, limit int) (jikan.ProducerListResult, error)
|
||||
GetGenres(ctx context.Context) ([]Genre, error)
|
||||
GetCharacters(ctx context.Context, id int) ([]Character, error)
|
||||
GetRecommendations(ctx context.Context, id int) ([]Recommendation, error)
|
||||
|
||||
Reference in New Issue
Block a user