feat: add watch-complete endpoint

Removes continue_watching_entry and clears progress when the last
episode finishes so it no longer shows in Continue Watching.
This commit is contained in:
2026-05-13 18:22:18 +02:00
parent 6c45a80623
commit 28e8b322d0
4 changed files with 46 additions and 0 deletions

View File

@@ -8,6 +8,7 @@ import (
type PlaybackService interface { type PlaybackService interface {
BuildWatchData(ctx context.Context, animeID int, titleCandidates []string, episode string, mode string, userID string) (map[string]any, error) BuildWatchData(ctx context.Context, animeID int, titleCandidates []string, episode string, mode string, userID string) (map[string]any, error)
SaveProgress(ctx context.Context, userID string, animeID int64, episode int, timeSeconds float64) error SaveProgress(ctx context.Context, userID string, animeID int64, episode int, timeSeconds float64) error
CompleteAnime(ctx context.Context, userID string, animeID int64) error
ResolveProxyToken(token string) (string, string, error) ResolveProxyToken(token string) (string, string, error)
} }
@@ -35,4 +36,5 @@ type PlaybackRepository interface {
GetContinueWatchingEntry(ctx context.Context, params db.GetContinueWatchingEntryParams) (db.ContinueWatchingEntry, error) GetContinueWatchingEntry(ctx context.Context, params db.GetContinueWatchingEntryParams) (db.ContinueWatchingEntry, error)
SaveWatchProgress(ctx context.Context, params db.SaveWatchProgressParams) error SaveWatchProgress(ctx context.Context, params db.SaveWatchProgressParams) error
UpsertContinueWatchingEntry(ctx context.Context, params db.UpsertContinueWatchingEntryParams) (db.ContinueWatchingEntry, error) UpsertContinueWatchingEntry(ctx context.Context, params db.UpsertContinueWatchingEntryParams) (db.ContinueWatchingEntry, error)
DeleteContinueWatchingEntry(ctx context.Context, params db.DeleteContinueWatchingEntryParams) error
} }

View File

@@ -36,6 +36,7 @@ func (h *PlaybackHandler) Register(r *gin.Engine) {
log.Println("Registering playback routes") log.Println("Registering playback routes")
r.GET("/anime/:id/watch", h.HandleWatchPage) r.GET("/anime/:id/watch", h.HandleWatchPage)
r.POST("/api/watch-progress", h.HandleSaveProgress) r.POST("/api/watch-progress", h.HandleSaveProgress)
r.POST("/api/watch-complete", h.HandleWatchComplete)
r.GET("/api/watch/thumbnails/:animeId", h.HandleEpisodeThumbnails) r.GET("/api/watch/thumbnails/:animeId", h.HandleEpisodeThumbnails)
r.GET("/watch/proxy/stream", h.HandleProxyStream) r.GET("/watch/proxy/stream", h.HandleProxyStream)
r.GET("/watch/proxy/subtitle", h.HandleProxySubtitle) r.GET("/watch/proxy/subtitle", h.HandleProxySubtitle)
@@ -111,6 +112,32 @@ func (h *PlaybackHandler) HandleSaveProgress(c *gin.Context) {
c.Status(http.StatusOK) c.Status(http.StatusOK)
} }
func (h *PlaybackHandler) HandleWatchComplete(c *gin.Context) {
user, _ := c.Get("User")
userID := ""
if u, ok := user.(*domain.User); ok {
userID = u.ID
}
var req struct {
MalID int64 `json:"mal_id"`
Episode int `json:"episode"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.Status(http.StatusBadRequest)
return
}
err := h.svc.CompleteAnime(c.Request.Context(), userID, req.MalID)
if err != nil {
c.Status(http.StatusInternalServerError)
return
}
c.Status(http.StatusOK)
}
func (h *PlaybackHandler) HandleEpisodeThumbnails(c *gin.Context) { func (h *PlaybackHandler) HandleEpisodeThumbnails(c *gin.Context) {
id, err := strconv.Atoi(c.Param("animeId")) id, err := strconv.Atoi(c.Param("animeId"))
if err != nil { if err != nil {

View File

@@ -29,3 +29,7 @@ func (r *playbackRepository) SaveWatchProgress(ctx context.Context, params db.Sa
func (r *playbackRepository) UpsertContinueWatchingEntry(ctx context.Context, params db.UpsertContinueWatchingEntryParams) (db.ContinueWatchingEntry, error) { func (r *playbackRepository) UpsertContinueWatchingEntry(ctx context.Context, params db.UpsertContinueWatchingEntryParams) (db.ContinueWatchingEntry, error) {
return r.queries.UpsertContinueWatchingEntry(ctx, params) return r.queries.UpsertContinueWatchingEntry(ctx, params)
} }
func (r *playbackRepository) DeleteContinueWatchingEntry(ctx context.Context, params db.DeleteContinueWatchingEntryParams) error {
return r.queries.DeleteContinueWatchingEntry(ctx, params)
}

View File

@@ -322,6 +322,19 @@ func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, title
}, nil }, nil
} }
func (s *playbackService) CompleteAnime(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,
})
}
func (s *playbackService) SaveProgress(ctx context.Context, userID string, animeID int64, episode int, timeSeconds float64) error { func (s *playbackService) SaveProgress(ctx context.Context, userID string, animeID int64, episode int, timeSeconds float64) error {
_, err := s.repo.UpsertContinueWatchingEntry(ctx, db.UpsertContinueWatchingEntryParams{ _, err := s.repo.UpsertContinueWatchingEntry(ctx, db.UpsertContinueWatchingEntryParams{
ID: uuid.New().String(), ID: uuid.New().String(),