refactor: extract watchlist map to service, optimize command palette queries

This commit is contained in:
2026-05-20 17:03:29 +02:00
committed by Mikkel Elvers
parent c4bd5cc395
commit 5482a40d47
5 changed files with 168 additions and 141 deletions

View File

@@ -26,6 +26,28 @@ func NewAnimeHandler(svc domain.AnimeService, watchlistSvc domain.WatchlistServi
} }
} }
func (h *AnimeHandler) watchlistMapForAnimes(ctx context.Context, userID string, animes []domain.Anime) map[int]bool {
animeIDs := make([]int64, 0, len(animes))
for _, anime := range animes {
if anime.MalID > 0 {
animeIDs = append(animeIDs, int64(anime.MalID))
}
}
return h.watchlistMapForIDs(ctx, userID, animeIDs)
}
func (h *AnimeHandler) watchlistMapForIDs(ctx context.Context, userID string, animeIDs []int64) map[int]bool {
if userID == "" || len(animeIDs) == 0 {
return map[int]bool{}
}
watchlistMap, err := h.watchlistSvc.GetWatchlistMap(ctx, userID, animeIDs)
if err != nil {
return map[int]bool{}
}
return watchlistMap
}
func (h *AnimeHandler) Register(r *gin.Engine) { func (h *AnimeHandler) Register(r *gin.Engine) {
r.GET("/", h.HandleCatalog) r.GET("/", h.HandleCatalog)
@@ -47,18 +69,11 @@ func (h *AnimeHandler) Register(r *gin.Engine) {
func (h *AnimeHandler) HandleCatalog(c *gin.Context) { func (h *AnimeHandler) HandleCatalog(c *gin.Context) {
user, _ := c.Get("User") user, _ := c.Get("User")
watchlistMap := make(map[int]bool)
if u, ok := user.(*domain.User); ok {
entries, _ := h.watchlistSvc.GetWatchlist(c.Request.Context(), u.ID)
for _, e := range entries {
watchlistMap[int(e.AnimeID)] = true
}
}
c.HTML(http.StatusOK, "index.gohtml", gin.H{ c.HTML(http.StatusOK, "index.gohtml", gin.H{
"CurrentPath": "/", "CurrentPath": "/",
"User": user, "User": user,
"WatchlistMap": watchlistMap, "WatchlistMap": map[int]bool{},
}) })
} }
@@ -85,12 +100,9 @@ func (h *AnimeHandler) renderCatalogSection(c *gin.Context, section string) {
return return
} }
watchlistMap := make(map[int]bool) watchlistMap := map[int]bool{}
if userID != "" { if animes, ok := data["Animes"].([]domain.Anime); ok {
entries, _ := h.watchlistSvc.GetWatchlist(c.Request.Context(), userID) watchlistMap = h.watchlistMapForAnimes(c.Request.Context(), userID, animes)
for _, e := range entries {
watchlistMap[int(e.AnimeID)] = true
}
} }
data["Section"] = section data["Section"] = section
@@ -130,12 +142,9 @@ func (h *AnimeHandler) renderDiscoverSection(c *gin.Context, section string) {
return return
} }
watchlistMap := make(map[int]bool) watchlistMap := map[int]bool{}
if userID != "" { if animes, ok := data["Animes"].([]domain.Anime); ok {
entries, _ := h.watchlistSvc.GetWatchlist(c.Request.Context(), userID) watchlistMap = h.watchlistMapForAnimes(c.Request.Context(), userID, animes)
for _, e := range entries {
watchlistMap[int(e.AnimeID)] = true
}
} }
data["Section"] = section data["Section"] = section
@@ -170,13 +179,11 @@ func (h *AnimeHandler) HandleBrowse(c *gin.Context) {
} }
user, _ := c.Get("User") user, _ := c.Get("User")
watchlistMap := make(map[int]bool) userID := ""
if u, ok := user.(*domain.User); ok { if u, ok := user.(*domain.User); ok {
entries, _ := h.watchlistSvc.GetWatchlist(c.Request.Context(), u.ID) userID = u.ID
for _, e := range entries {
watchlistMap[int(e.AnimeID)] = true
}
} }
watchlistMap := h.watchlistMapForAnimes(c.Request.Context(), userID, res.Animes)
if c.GetHeader("HX-Request") == "true" && page > 1 { if c.GetHeader("HX-Request") == "true" && page > 1 {
c.HTML(http.StatusOK, "browse.gohtml", gin.H{ c.HTML(http.StatusOK, "browse.gohtml", gin.H{
@@ -246,27 +253,30 @@ func (h *AnimeHandler) HandleAnimeDetails(c *gin.Context) {
section := c.Query("section") section := c.Query("section")
if section != "" && c.GetHeader("HX-Request") == "true" { if section != "" && c.GetHeader("HX-Request") == "true" {
sectionCtx, cancel := context.WithTimeout(c.Request.Context(), 4*time.Second)
defer cancel()
var data any var data any
var tplName string var tplName string
var err error var err error
switch section { switch section {
case "characters": case "characters":
data, err = h.svc.GetCharacters(c.Request.Context(), id) data, err = h.svc.GetCharacters(sectionCtx, id)
tplName = "anime_characters" tplName = "anime_characters"
case "recommendations": case "recommendations":
data, err = h.svc.GetRecommendations(c.Request.Context(), id) data, err = h.svc.GetRecommendations(sectionCtx, id)
tplName = "anime_recommendations" tplName = "anime_recommendations"
case "statistics": case "statistics":
data, err = h.svc.GetStatistics(c.Request.Context(), id) data, err = h.svc.GetStatistics(sectionCtx, id)
tplName = "anime_statistics" tplName = "anime_statistics"
case "themes": case "themes":
data, err = h.svc.GetThemes(c.Request.Context(), id) data, err = h.svc.GetThemes(sectionCtx, id)
tplName = "anime_themes" tplName = "anime_themes"
} }
if err != nil { if err != nil {
c.Status(http.StatusInternalServerError) c.String(http.StatusOK, "")
return return
} }
@@ -326,19 +336,22 @@ func (h *AnimeHandler) HandleHTMLWatchOrder(c *gin.Context) {
userID = u.ID userID = u.ID
} }
relations, err := h.svc.GetRelations(c.Request.Context(), id) relationsCtx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
relations, err := h.svc.GetRelations(relationsCtx, id)
if err != nil { if err != nil {
c.Status(http.StatusInternalServerError) c.String(http.StatusOK, "")
return return
} }
watchlistMap := make(map[int]bool) relationAnimeIDs := make([]int64, 0, len(relations))
if userID != "" { for _, relation := range relations {
entries, _ := h.watchlistSvc.GetWatchlist(c.Request.Context(), userID) if relation.Anime.MalID > 0 {
for _, e := range entries { relationAnimeIDs = append(relationAnimeIDs, int64(relation.Anime.MalID))
watchlistMap[int(e.AnimeID)] = true
} }
} }
watchlistMap := h.watchlistMapForIDs(c.Request.Context(), userID, relationAnimeIDs)
c.HTML(http.StatusOK, "anime.gohtml", gin.H{ c.HTML(http.StatusOK, "anime.gohtml", gin.H{
"_fragment": "watch_order", "_fragment": "watch_order",
@@ -362,13 +375,11 @@ func (h *AnimeHandler) HandleQuickSearch(c *gin.Context) {
} }
user, _ := c.Get("User") user, _ := c.Get("User")
watchlistMap := make(map[int]bool) userID := ""
if u, ok := user.(*domain.User); ok { if u, ok := user.(*domain.User); ok {
entries, _ := h.watchlistSvc.GetWatchlist(c.Request.Context(), u.ID) userID = u.ID
for _, e := range entries {
watchlistMap[int(e.AnimeID)] = true
}
} }
watchlistMap := h.watchlistMapForAnimes(c.Request.Context(), userID, res.Animes)
type quickSearchResult struct { type quickSearchResult struct {
ID int `json:"id"` ID int `json:"id"`
@@ -462,7 +473,7 @@ func (h *AnimeHandler) commandPaletteNavigationItems(query string) []commandPale
} }
func (h *AnimeHandler) commandPaletteAnimeResults(c *gin.Context, query string) []commandPaletteItem { func (h *AnimeHandler) commandPaletteAnimeResults(c *gin.Context, query string) []commandPaletteItem {
searchCtx, cancel := context.WithTimeout(c.Request.Context(), 1500*time.Millisecond) searchCtx, cancel := context.WithTimeout(c.Request.Context(), 800*time.Millisecond)
defer cancel() defer cancel()
res, err := h.svc.SearchAdvanced(searchCtx, query, "", "", "", "", nil, true, 1, 5) res, err := h.svc.SearchAdvanced(searchCtx, query, "", "", "", "", nil, true, 1, 5)
@@ -487,35 +498,23 @@ func (h *AnimeHandler) commandPaletteAnimeResults(c *gin.Context, query string)
func (h *AnimeHandler) commandPalettePersonalItems(c *gin.Context, userID string, query string) []commandPaletteItem { func (h *AnimeHandler) commandPalettePersonalItems(c *gin.Context, userID string, query string) []commandPaletteItem {
items := make([]commandPaletteItem, 0, 5) items := make([]commandPaletteItem, 0, 5)
watchlist, err := h.watchlistSvc.GetWatchlist(c.Request.Context(), userID) watchlist, err := h.watchlistSvc.GetCommandPaletteWatchlist(c.Request.Context(), userID, query, 5)
if err != nil { if err != nil {
return items return items
} }
watchlistCount := 0 for _, entry := range watchlist {
for _, status := range []string{"watching", "plan_to_watch"} { title := watchlistTitle(entry)
for _, entry := range watchlist { items = append(items, commandPaletteItem{
if watchlistCount >= 5 { ID: fmt.Sprintf("watchlist:%d", entry.AnimeID),
return items Type: "watchlist",
} Label: title,
if entry.Status != status { Subtitle: watchlistStatusLabel(entry.Status),
continue Href: fmt.Sprintf("/anime/%d", entry.AnimeID),
} Image: entry.ImageUrl,
})
title := watchlistTitle(entry) if len(items) >= 5 {
if query != "" && !commandPaletteMatches(query, title, entry.Status) { return items
continue
}
items = append(items, commandPaletteItem{
ID: fmt.Sprintf("watchlist:%d", entry.AnimeID),
Type: "watchlist",
Label: title,
Subtitle: watchlistStatusLabel(entry.Status),
Href: fmt.Sprintf("/anime/%d", entry.AnimeID),
Image: entry.ImageUrl,
})
watchlistCount++
} }
} }
@@ -525,33 +524,29 @@ func (h *AnimeHandler) commandPalettePersonalItems(c *gin.Context, userID string
func (h *AnimeHandler) commandPaletteContinueItems(c *gin.Context, userID string, query string) []commandPaletteItem { func (h *AnimeHandler) commandPaletteContinueItems(c *gin.Context, userID string, query string) []commandPaletteItem {
items := make([]commandPaletteItem, 0, 5) items := make([]commandPaletteItem, 0, 5)
data, err := h.svc.GetCatalogSection(c.Request.Context(), userID, "Continue") rows, err := h.watchlistSvc.GetCommandPaletteContinueWatching(c.Request.Context(), userID, query, 5)
if err == nil { if err != nil {
if rows, ok := data["ContinueWatching"].([]db.GetContinueWatchingEntriesRow); ok { return items
for _, row := range rows { }
if len(items) >= 5 {
break
}
title := continueWatchingTitle(row) for _, row := range rows {
if query != "" && !commandPaletteMatches(query, title, "Continue watching") { title := continueWatchingTitle(row)
continue episode := ""
} href := fmt.Sprintf("/anime/%d/watch", row.AnimeID)
episode := "" if row.CurrentEpisode.Valid {
href := fmt.Sprintf("/anime/%d/watch", row.AnimeID) episode = fmt.Sprintf(" episode %d", row.CurrentEpisode.Int64)
if row.CurrentEpisode.Valid { href = fmt.Sprintf("%s?ep=%d", href, row.CurrentEpisode.Int64)
episode = fmt.Sprintf(" episode %d", row.CurrentEpisode.Int64) }
href = fmt.Sprintf("%s?ep=%d", href, row.CurrentEpisode.Int64) items = append(items, commandPaletteItem{
} ID: fmt.Sprintf("continue:%d", row.AnimeID),
items = append(items, commandPaletteItem{ Type: "continue",
ID: fmt.Sprintf("continue:%d", row.AnimeID), Label: "Continue watching " + title,
Type: "continue", Subtitle: "Resume" + episode,
Label: "Continue watching " + title, Href: href,
Subtitle: "Resume" + episode, Image: row.ImageUrl,
Href: href, })
Image: row.ImageUrl, if len(items) >= 5 {
}) return items
}
} }
} }
@@ -594,7 +589,10 @@ func watchlistStatusLabel(status string) string {
} }
func (h *AnimeHandler) HandleRandomAnime(c *gin.Context) { func (h *AnimeHandler) HandleRandomAnime(c *gin.Context) {
anime, err := h.svc.GetRandomAnime(c.Request.Context()) ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
anime, err := h.svc.GetRandomAnime(ctx)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch random anime"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch random anime"})
return return
@@ -607,13 +605,8 @@ func (h *AnimeHandler) HandleRandomAnime(c *gin.Context) {
user, _ := c.Get("User") user, _ := c.Get("User")
inWatchlist := false inWatchlist := false
if u, ok := user.(*domain.User); ok { if u, ok := user.(*domain.User); ok {
entries, _ := h.watchlistSvc.GetWatchlist(c.Request.Context(), u.ID) watchlistMap := h.watchlistMapForIDs(c.Request.Context(), u.ID, []int64{int64(anime.MalID)})
for _, e := range entries { inWatchlist = watchlistMap[anime.MalID]
if int(e.AnimeID) == anime.MalID {
inWatchlist = true
break
}
}
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{

View File

@@ -5,6 +5,8 @@ import (
"mal/integrations/jikan" "mal/integrations/jikan"
"mal/internal/db" "mal/internal/db"
"mal/internal/domain" "mal/internal/domain"
"math/rand"
"time"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
@@ -20,9 +22,8 @@ func NewAnimeService(jikan *jikan.Client, repo domain.AnimeRepository) domain.An
func (s *animeService) GetCatalogSection(ctx context.Context, userID string, section string) (map[string]any, error) { func (s *animeService) GetCatalogSection(ctx context.Context, userID string, section string) (map[string]any, error) {
var ( var (
res jikan.TopAnimeResult res jikan.TopAnimeResult
cw []db.GetContinueWatchingEntriesRow cw []db.GetContinueWatchingEntriesRow
watchlist []db.GetUserWatchListRow
) )
g, gCtx := errgroup.WithContext(ctx) g, gCtx := errgroup.WithContext(ctx)
@@ -38,18 +39,10 @@ func (s *animeService) GetCatalogSection(ctx context.Context, userID string, sec
return err return err
}) })
if userID != "" { if userID != "" && section == "Continue" {
g.Go(func() error {
if section == "Continue" {
var err error
cw, err = s.repo.GetContinueWatchingEntries(gCtx, userID)
return err
}
return nil
})
g.Go(func() error { g.Go(func() error {
var err error var err error
watchlist, err = s.repo.GetUserWatchList(gCtx, userID) cw, err = s.repo.GetContinueWatchingEntries(gCtx, userID)
return err return err
}) })
} }
@@ -63,23 +56,14 @@ func (s *animeService) GetCatalogSection(ctx context.Context, userID string, sec
animes = animes[:6] animes = animes[:6]
} }
watchlistMap := make(map[int64]bool)
for _, entry := range watchlist {
watchlistMap[entry.AnimeID] = true
}
return map[string]any{ return map[string]any{
"Animes": animes, "Animes": animes,
"ContinueWatching": cw, "ContinueWatching": cw,
"WatchlistMap": watchlistMap,
}, nil }, nil
} }
func (s *animeService) GetDiscoverSection(ctx context.Context, userID string, section string) (map[string]any, error) { func (s *animeService) GetDiscoverSection(ctx context.Context, userID string, section string) (map[string]any, error) {
var ( var res jikan.TopAnimeResult
res jikan.TopAnimeResult
watchlist []db.GetUserWatchListRow
)
g, gCtx := errgroup.WithContext(ctx) g, gCtx := errgroup.WithContext(ctx)
@@ -96,14 +80,6 @@ func (s *animeService) GetDiscoverSection(ctx context.Context, userID string, se
return err return err
}) })
if userID != "" {
g.Go(func() error {
var err error
watchlist, err = s.repo.GetUserWatchList(gCtx, userID)
return err
})
}
if err := g.Wait(); err != nil { if err := g.Wait(); err != nil {
return nil, err return nil, err
} }
@@ -113,14 +89,8 @@ func (s *animeService) GetDiscoverSection(ctx context.Context, userID string, se
animes = animes[:8] animes = animes[:8]
} }
watchlistMap := make(map[int64]bool)
for _, entry := range watchlist {
watchlistMap[entry.AnimeID] = true
}
return map[string]any{ return map[string]any{
"Animes": animes, "Animes": animes,
"WatchlistMap": watchlistMap,
}, nil }, nil
} }
@@ -173,7 +143,27 @@ func (s *animeService) GetReviews(ctx context.Context, id int, page int) ([]doma
} }
func (s *animeService) GetRandomAnime(ctx context.Context) (domain.Anime, error) { func (s *animeService) GetRandomAnime(ctx context.Context) (domain.Anime, error) {
return s.jikan.GetRandomAnime(ctx) randomCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
anime, err := s.jikan.GetRandomAnime(randomCtx)
if err == nil {
return anime, nil
}
for _, fallback := range []func(context.Context, int) (jikan.TopAnimeResult, error){
s.jikan.GetSeasonsNow,
s.jikan.GetTopAnime,
s.jikan.GetSeasonsUpcoming,
} {
res, fallbackErr := fallback(ctx, 1)
if fallbackErr != nil || len(res.Animes) == 0 {
continue
}
return res.Animes[rand.Intn(len(res.Animes))], nil
}
return domain.Anime{}, err
} }
func (s *animeService) GetAllEpisodes(ctx context.Context, id int) ([]domain.EpisodeData, error) { func (s *animeService) GetAllEpisodes(ctx context.Context, id int) ([]domain.EpisodeData, error) {

View File

@@ -12,6 +12,9 @@ type WatchlistService interface {
UpdateEntry(ctx context.Context, userID string, animeID int64, status string) error UpdateEntry(ctx context.Context, userID string, animeID int64, status string) error
RemoveEntry(ctx context.Context, userID string, animeID int64) error RemoveEntry(ctx context.Context, userID string, animeID int64) error
GetWatchlist(ctx context.Context, userID string) ([]UserWatchListRow, error) GetWatchlist(ctx context.Context, userID string) ([]UserWatchListRow, error)
GetWatchlistMap(ctx context.Context, userID string, animeIDs []int64) (map[int]bool, error)
GetCommandPaletteWatchlist(ctx context.Context, userID string, query string, limit int64) ([]UserWatchListRow, error)
GetCommandPaletteContinueWatching(ctx context.Context, userID string, query string, limit int64) ([]db.GetContinueWatchingEntriesRow, error)
GetWatchListEntry(ctx context.Context, userID string, animeID int64) (WatchlistEntry, error) GetWatchListEntry(ctx context.Context, userID string, animeID int64) (WatchlistEntry, error)
GetContinueWatchingEntry(ctx context.Context, userID string, animeID int64) (db.ContinueWatchingEntry, error) GetContinueWatchingEntry(ctx context.Context, userID string, animeID int64) (db.ContinueWatchingEntry, error)
DeleteContinueWatching(ctx context.Context, userID string, animeID int64) error DeleteContinueWatching(ctx context.Context, userID string, animeID int64) error
@@ -23,6 +26,9 @@ type WatchlistRepository interface {
UpsertWatchListEntry(ctx context.Context, arg db.UpsertWatchListEntryParams) (db.WatchListEntry, error) UpsertWatchListEntry(ctx context.Context, arg db.UpsertWatchListEntryParams) (db.WatchListEntry, error)
DeleteWatchListEntry(ctx context.Context, arg db.DeleteWatchListEntryParams) error DeleteWatchListEntry(ctx context.Context, arg db.DeleteWatchListEntryParams) error
GetUserWatchList(ctx context.Context, userID string) ([]db.GetUserWatchListRow, error) GetUserWatchList(ctx context.Context, userID string) ([]db.GetUserWatchListRow, error)
GetUserWatchlistAnimeIDs(ctx context.Context, userID string, animeIDs []int64) ([]int64, error)
GetCommandPaletteWatchlist(ctx context.Context, userID string, query string, limit int64) ([]db.GetUserWatchListRow, error)
GetCommandPaletteContinueWatching(ctx context.Context, userID string, query string, limit int64) ([]db.GetContinueWatchingEntriesRow, error)
GetWatchListEntry(ctx context.Context, arg db.GetWatchListEntryParams) (db.WatchListEntry, error) GetWatchListEntry(ctx context.Context, arg db.GetWatchListEntryParams) (db.WatchListEntry, error)
GetContinueWatchingEntry(ctx context.Context, arg db.GetContinueWatchingEntryParams) (db.ContinueWatchingEntry, error) GetContinueWatchingEntry(ctx context.Context, arg db.GetContinueWatchingEntryParams) (db.ContinueWatchingEntry, error)
DeleteContinueWatchingEntry(ctx context.Context, arg db.DeleteContinueWatchingEntryParams) error DeleteContinueWatchingEntry(ctx context.Context, arg db.DeleteContinueWatchingEntryParams) error

View File

@@ -34,6 +34,18 @@ func (r *watchlistRepository) GetUserWatchList(ctx context.Context, userID strin
return r.queries.GetUserWatchList(ctx, userID) return r.queries.GetUserWatchList(ctx, userID)
} }
func (r *watchlistRepository) GetUserWatchlistAnimeIDs(ctx context.Context, userID string, animeIDs []int64) ([]int64, error) {
return r.queries.GetUserWatchlistAnimeIDs(ctx, userID, animeIDs)
}
func (r *watchlistRepository) GetCommandPaletteWatchlist(ctx context.Context, userID string, query string, limit int64) ([]db.GetUserWatchListRow, error) {
return r.queries.GetCommandPaletteWatchlist(ctx, userID, query, limit)
}
func (r *watchlistRepository) GetCommandPaletteContinueWatching(ctx context.Context, userID string, query string, limit int64) ([]db.GetContinueWatchingEntriesRow, error) {
return r.queries.GetCommandPaletteContinueWatching(ctx, userID, query, limit)
}
func (r *watchlistRepository) GetWatchListEntry(ctx context.Context, arg db.GetWatchListEntryParams) (db.WatchListEntry, error) { func (r *watchlistRepository) GetWatchListEntry(ctx context.Context, arg db.GetWatchListEntryParams) (db.WatchListEntry, error) {
return r.queries.GetWatchListEntry(ctx, arg) return r.queries.GetWatchListEntry(ctx, arg)
} }

View File

@@ -55,6 +55,32 @@ func (s *watchlistService) GetWatchlist(ctx context.Context, userID string) ([]d
return s.repo.GetUserWatchList(ctx, userID) return s.repo.GetUserWatchList(ctx, userID)
} }
func (s *watchlistService) GetWatchlistMap(ctx context.Context, userID string, animeIDs []int64) (map[int]bool, error) {
watchlistMap := make(map[int]bool)
if userID == "" || len(animeIDs) == 0 {
return watchlistMap, nil
}
matches, err := s.repo.GetUserWatchlistAnimeIDs(ctx, userID, animeIDs)
if err != nil {
return watchlistMap, err
}
for _, animeID := range matches {
watchlistMap[int(animeID)] = true
}
return watchlistMap, nil
}
func (s *watchlistService) GetCommandPaletteWatchlist(ctx context.Context, userID string, query string, limit int64) ([]domain.UserWatchListRow, error) {
return s.repo.GetCommandPaletteWatchlist(ctx, userID, query, limit)
}
func (s *watchlistService) GetCommandPaletteContinueWatching(ctx context.Context, userID string, query string, limit int64) ([]db.GetContinueWatchingEntriesRow, error) {
return s.repo.GetCommandPaletteContinueWatching(ctx, userID, query, limit)
}
func (s *watchlistService) GetWatchListEntry(ctx context.Context, userID string, animeID int64) (db.WatchListEntry, error) { func (s *watchlistService) GetWatchListEntry(ctx context.Context, userID string, animeID int64) (db.WatchListEntry, error) {
return s.repo.GetWatchListEntry(ctx, db.GetWatchListEntryParams{ return s.repo.GetWatchListEntry(ctx, db.GetWatchListEntryParams{
UserID: userID, UserID: userID,