Files
mal/internal/watchlist/service.go

161 lines
4.9 KiB
Go

package watchlist
import (
"context"
"database/sql"
"mal/integrations/jikan"
"mal/internal/db"
"mal/internal/domain"
"github.com/google/uuid"
)
type watchlistService struct {
repo domain.WatchlistRepository
jikan *jikan.Client
invalidator domain.RecommendationInvalidator
}
func NewWatchlistService(repo domain.WatchlistRepository, jikan *jikan.Client, invalidator domain.RecommendationInvalidator) domain.WatchlistService {
return &watchlistService{repo: repo, jikan: jikan, invalidator: invalidator}
}
func (s *watchlistService) UpdateEntry(ctx context.Context, userID string, animeID int64, status string) error {
anime, fetchedAnime := s.fetchAnimeForWatchlist(ctx, animeID)
if err := s.repo.InTx(ctx, func(txCtx context.Context, repo domain.WatchlistRepository) error {
return s.updateEntryInTx(txCtx, repo, userID, animeID, status, anime, fetchedAnime)
}); err != nil {
return err
}
s.invalidateTopPicks(userID)
return nil
}
func (s *watchlistService) fetchAnimeForWatchlist(ctx context.Context, animeID int64) (jikan.Anime, bool) {
if s.jikan == nil {
return jikan.Anime{}, false
}
anime, err := s.jikan.GetAnimeByID(ctx, int(animeID))
if err != nil {
// still allow status updates for already-known anime rows
return jikan.Anime{}, false
}
return anime, anime.MalID > 0
}
func (s *watchlistService) updateEntryInTx(ctx context.Context, repo domain.WatchlistRepository, userID string, animeID int64, status string, anime jikan.Anime, fetchedAnime bool) error {
_, err := repo.GetAnime(ctx, animeID)
if err != nil && fetchedAnime {
if _, err := repo.UpsertAnime(ctx, watchlistAnimeParams(anime)); err != nil {
return err
}
}
existing, err := repo.GetWatchListEntry(ctx, db.GetWatchListEntryParams{
UserID: userID,
AnimeID: animeID,
})
if err != nil && err != sql.ErrNoRows {
return err
}
_, err = repo.UpsertWatchListEntry(ctx, db.UpsertWatchListEntryParams{
ID: uuid.New().String(),
UserID: userID,
AnimeID: animeID,
Status: status,
CurrentEpisode: existing.CurrentEpisode,
CurrentTimeSeconds: existing.CurrentTimeSeconds,
})
return err
}
func watchlistAnimeParams(anime jikan.Anime) db.UpsertAnimeParams {
durationSeconds := anime.DurationSeconds()
duration := sql.NullFloat64{Valid: durationSeconds > 0}
if duration.Valid {
duration.Float64 = durationSeconds
}
return db.UpsertAnimeParams{
ID: int64(anime.MalID),
TitleOriginal: anime.Title,
TitleEnglish: sql.NullString{String: anime.TitleEnglish, Valid: anime.TitleEnglish != ""},
TitleJapanese: sql.NullString{String: anime.TitleJapanese, Valid: anime.TitleJapanese != ""},
ImageUrl: anime.Images.Webp.LargeImageURL,
Airing: sql.NullBool{Bool: anime.Airing, Valid: true},
DurationSeconds: duration,
}
}
func (s *watchlistService) RemoveEntry(ctx context.Context, userID string, animeID int64) error {
if err := s.repo.DeleteWatchListEntry(ctx, db.DeleteWatchListEntryParams{
UserID: userID,
AnimeID: animeID,
}); err != nil {
return err
}
s.invalidateTopPicks(userID)
return nil
}
func (s *watchlistService) invalidateTopPicks(userID string) {
if s.invalidator != nil {
s.invalidator.InvalidateTopPicksForUser(userID)
}
}
func (s *watchlistService) GetWatchlist(ctx context.Context, userID string) ([]domain.UserWatchListRow, error) {
return s.repo.GetUserWatchList(ctx, userID)
}
func (s *watchlistService) GetWatchlistMap(ctx context.Context, userID string, animeIDs []int64) (map[int64]bool, error) {
watchlistMap := make(map[int64]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[animeID] = true
}
return watchlistMap, nil
}
func (s *watchlistService) GetWatchListEntry(ctx context.Context, userID string, animeID int64) (db.WatchListEntry, error) {
return s.repo.GetWatchListEntry(ctx, db.GetWatchListEntryParams{
UserID: userID,
AnimeID: animeID,
})
}
func (s *watchlistService) GetContinueWatchingEntry(ctx context.Context, userID string, animeID int64) (db.ContinueWatchingEntry, error) {
return s.repo.GetContinueWatchingEntry(ctx, db.GetContinueWatchingEntryParams{
UserID: userID,
AnimeID: animeID,
})
}
func (s *watchlistService) DeleteContinueWatching(ctx context.Context, userID string, animeID int64) error {
return s.repo.InTx(ctx, func(txCtx context.Context, repo domain.WatchlistRepository) error {
if err := repo.DeleteContinueWatchingEntry(txCtx, db.DeleteContinueWatchingEntryParams{
UserID: userID,
AnimeID: animeID,
}); err != nil {
return err
}
return repo.SaveWatchProgress(txCtx, db.SaveWatchProgressParams{
UserID: userID,
AnimeID: animeID,
CurrentEpisode: sql.NullInt64{Valid: false},
CurrentTimeSeconds: 0,
})
})
}