feat: migrate watchlist module to modular domain pattern

This commit is contained in:
2026-05-13 10:33:24 +02:00
parent c32ffd54de
commit c94a2fed04
6 changed files with 256 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ import (
"mal/internal/database"
"mal/internal/auth"
"mal/internal/anime"
"mal/internal/watchlist"
"mal/internal/server"
"mal/internal/templates"
@@ -18,6 +19,7 @@ func NewApp() *fx.App {
jikan.Module,
auth.Module,
anime.Module,
watchlist.Module,
templates.Module,
server.Module,
fx.Decorate(func(r *templates.Renderer) render.HTMLRender {

View File

@@ -0,0 +1,26 @@
package domain
import (
"context"
"mal/internal/db"
)
type WatchlistEntry = db.WatchListEntry
type UserWatchListRow = db.GetUserWatchListRow
type WatchlistService interface {
UpdateEntry(ctx context.Context, userID string, animeID int64, status string) error
RemoveEntry(ctx context.Context, userID string, animeID int64) error
GetWatchlist(ctx context.Context, userID string) ([]UserWatchListRow, error)
DeleteContinueWatching(ctx context.Context, userID string, animeID int64) error
}
type WatchlistRepository interface {
UpsertAnime(ctx context.Context, arg db.UpsertAnimeParams) (db.Anime, error)
GetAnime(ctx context.Context, id int64) (db.Anime, error)
UpsertWatchListEntry(ctx context.Context, arg db.UpsertWatchListEntryParams) (db.WatchListEntry, error)
DeleteWatchListEntry(ctx context.Context, arg db.DeleteWatchListEntryParams) error
GetUserWatchList(ctx context.Context, userID string) ([]db.GetUserWatchListRow, error)
DeleteContinueWatchingEntry(ctx context.Context, arg db.DeleteContinueWatchingEntryParams) error
SaveWatchProgress(ctx context.Context, arg db.SaveWatchProgressParams) error
}

View File

@@ -0,0 +1,95 @@
package handler
import (
"mal/internal/domain"
"mal/internal/server"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
type WatchlistHandler struct {
svc domain.WatchlistService
}
func NewWatchlistHandler(svc domain.WatchlistService) *WatchlistHandler {
return &WatchlistHandler{svc: svc}
}
func (h *WatchlistHandler) Register(r *gin.Engine) {
r.POST("/api/watchlist", h.HandleUpdateWatchlist)
r.DELETE("/api/watchlist/:id", h.HandleDeleteWatchlist)
r.DELETE("/api/continue-watching/:id", h.HandleDeleteContinueWatching)
r.GET("/watchlist", h.HandleGetWatchlist)
}
func (h *WatchlistHandler) HandleUpdateWatchlist(c *gin.Context) {
userID := "" // TODO: get from auth context
animeID, _ := strconv.ParseInt(c.PostForm("anime_id"), 10, 64)
status := c.PostForm("status")
if animeID <= 0 || status == "" {
c.Status(http.StatusBadRequest)
return
}
err := h.svc.UpdateEntry(c.Request.Context(), userID, animeID, status)
if err != nil {
c.Status(http.StatusInternalServerError)
return
}
c.Status(http.StatusOK)
}
func (h *WatchlistHandler) HandleDeleteWatchlist(c *gin.Context) {
userID := "" // TODO: get from auth context
animeID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if animeID <= 0 {
c.Status(http.StatusBadRequest)
return
}
err := h.svc.RemoveEntry(c.Request.Context(), userID, animeID)
if err != nil {
c.Status(http.StatusInternalServerError)
return
}
c.Status(http.StatusOK)
}
func (h *WatchlistHandler) HandleDeleteContinueWatching(c *gin.Context) {
userID := "" // TODO: get from auth context
animeID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if animeID <= 0 {
c.Status(http.StatusBadRequest)
return
}
err := h.svc.DeleteContinueWatching(c.Request.Context(), userID, animeID)
if err != nil {
c.Status(http.StatusInternalServerError)
return
}
c.Status(http.StatusOK)
}
func (h *WatchlistHandler) HandleGetWatchlist(c *gin.Context) {
userID := "" // TODO: get from auth context
entries, err := h.svc.GetWatchlist(c.Request.Context(), userID)
if err != nil {
c.Status(http.StatusInternalServerError)
return
}
c.HTML(http.StatusOK, "watchlist.gohtml", gin.H{
"Entries": entries,
"CurrentPath": "/watchlist",
})
}

View File

@@ -0,0 +1,23 @@
package watchlist
import (
"mal/internal/server"
"mal/internal/watchlist/handler"
"mal/internal/watchlist/repository"
"mal/internal/watchlist/service"
"go.uber.org/fx"
)
var Module = fx.Options(
fx.Provide(
repository.NewWatchlistRepository,
service.NewWatchlistService,
handler.NewWatchlistHandler,
),
fx.Provide(
server.AsRouteRegister(func(h *handler.WatchlistHandler) server.RouteRegister {
return h
}),
),
)

View File

@@ -0,0 +1,43 @@
package repository
import (
"context"
"mal/internal/db"
"mal/internal/domain"
)
type watchlistRepository struct {
queries *db.Queries
}
func NewWatchlistRepository(queries *db.Queries) domain.WatchlistRepository {
return &watchlistRepository{queries: queries}
}
func (r *watchlistRepository) UpsertAnime(ctx context.Context, arg db.UpsertAnimeParams) (db.Anime, error) {
return r.queries.UpsertAnime(ctx, arg)
}
func (r *watchlistRepository) GetAnime(ctx context.Context, id int64) (db.Anime, error) {
return r.queries.GetAnime(ctx, id)
}
func (r *watchlistRepository) UpsertWatchListEntry(ctx context.Context, arg db.UpsertWatchListEntryParams) (db.WatchListEntry, error) {
return r.queries.UpsertWatchListEntry(ctx, arg)
}
func (r *watchlistRepository) DeleteWatchListEntry(ctx context.Context, arg db.DeleteWatchListEntryParams) error {
return r.queries.DeleteWatchListEntry(ctx, arg)
}
func (r *watchlistRepository) GetUserWatchList(ctx context.Context, userID string) ([]db.GetUserWatchListRow, error) {
return r.queries.GetUserWatchList(ctx, userID)
}
func (r *watchlistRepository) DeleteContinueWatchingEntry(ctx context.Context, arg db.DeleteContinueWatchingEntryParams) error {
return r.queries.DeleteContinueWatchingEntry(ctx, arg)
}
func (r *watchlistRepository) SaveWatchProgress(ctx context.Context, arg db.SaveWatchProgressParams) error {
return r.queries.SaveWatchProgress(ctx, arg)
}

View File

@@ -0,0 +1,67 @@
package service
import (
"context"
"database/sql"
"fmt"
"mal/integrations/jikan"
"mal/internal/db"
"mal/internal/domain"
)
type watchlistService struct {
repo domain.WatchlistRepository
jikan *jikan.Client
}
func NewWatchlistService(repo domain.WatchlistRepository, jikan *jikan.Client) domain.WatchlistService {
return &watchlistService{repo: repo, jikan: jikan}
}
func (s *watchlistService) UpdateEntry(ctx context.Context, userID string, animeID int64, status string) error {
_, err := s.repo.GetAnime(ctx, animeID)
if err != nil {
anime, err := s.jikan.GetAnimeByID(ctx, int(animeID))
if err == nil {
_, _ = s.repo.UpsertAnime(ctx, 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.ImageURL(),
Airing: sql.NullBool{Bool: anime.Airing, Valid: true},
})
}
}
_, err = s.repo.UpsertWatchListEntry(ctx, db.UpsertWatchListEntryParams{
UserID: userID,
AnimeID: animeID,
Status: status,
})
return err
}
func (s *watchlistService) RemoveEntry(ctx context.Context, userID string, animeID int64) error {
return s.repo.DeleteWatchListEntry(ctx, db.DeleteWatchListEntryParams{
UserID: userID,
AnimeID: animeID,
})
}
func (s *watchlistService) GetWatchlist(ctx context.Context, userID string) ([]domain.UserWatchListRow, error) {
return s.repo.GetUserWatchList(ctx, userID)
}
func (s *watchlistService) DeleteContinueWatching(ctx context.Context, userID string, animeID int64) error {
_ = s.repo.DeleteContinueWatchingEntry(ctx, db.DeleteContinueWatchingEntryParams{
UserID: userID,
AnimeID: animeID,
})
return s.repo.SaveWatchProgress(ctx, db.SaveWatchProgressParams{
UserID: userID,
AnimeID: animeID,
CurrentEpisode: sql.NullInt64{Valid: false},
CurrentTimeSeconds: 0,
})
}