feat: add skip segment overrides backend

This commit is contained in:
2026-05-19 11:02:54 +02:00
parent bcee65cbad
commit 1a9e1d60bc
6 changed files with 215 additions and 2 deletions

View File

@@ -41,6 +41,7 @@ func (h *PlaybackHandler) Register(r *gin.Engine) {
r.POST("/api/watch-progress", h.HandleSaveProgress)
r.POST("/api/watch-complete", h.HandleWatchComplete)
r.GET("/api/watch/episode/:animeId/:episode", h.HandleEpisodeData)
r.POST("/api/watch/segments", h.HandleUpsertSkipSegment)
r.GET("/api/watch/thumbnails/:animeId", h.HandleEpisodeThumbnails)
r.GET("/watch/proxy/stream", h.HandleProxyStream)
r.GET("/watch/proxy/subtitle", h.HandleProxySubtitle)
@@ -197,6 +198,37 @@ func (h *PlaybackHandler) HandleWatchComplete(c *gin.Context) {
c.Status(http.StatusOK)
}
func (h *PlaybackHandler) HandleUpsertSkipSegment(c *gin.Context) {
user, _ := c.Get("User")
userID := ""
if u, ok := user.(*domain.User); ok {
userID = u.ID
}
if userID == "" {
c.Status(http.StatusUnauthorized)
return
}
var req struct {
MalID int64 `json:"mal_id"`
Episode int `json:"episode"`
SkipType string `json:"skip_type"` // 'op' or 'ed' (also accepts 'opening'/'ending')
StartTime float64 `json:"start_time"`
EndTime float64 `json:"end_time"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.Status(http.StatusBadRequest)
return
}
if err := h.svc.UpsertSkipSegmentOverride(c.Request.Context(), userID, req.MalID, req.Episode, req.SkipType, req.StartTime, req.EndTime); err != nil {
c.Status(http.StatusBadRequest)
return
}
c.Status(http.StatusOK)
}
func (h *PlaybackHandler) HandleEpisodeThumbnails(c *gin.Context) {
id, err := strconv.Atoi(c.Param("animeId"))
if err != nil {

View File

@@ -37,3 +37,15 @@ func (r *playbackRepository) UpsertContinueWatchingEntry(ctx context.Context, pa
func (r *playbackRepository) DeleteContinueWatchingEntry(ctx context.Context, params db.DeleteContinueWatchingEntryParams) error {
return r.queries.DeleteContinueWatchingEntry(ctx, params)
}
func (r *playbackRepository) ListSkipSegmentOverrides(ctx context.Context, userID string, animeID int64, episode int64) ([]db.SkipSegmentOverride, error) {
return r.queries.ListSkipSegmentOverrides(ctx, userID, animeID, episode)
}
func (r *playbackRepository) UpsertSkipSegmentOverride(ctx context.Context, s db.SkipSegmentOverride) error {
return r.queries.UpsertSkipSegmentOverride(ctx, s)
}
func (r *playbackRepository) HasSkipSegmentOverrideTable(ctx context.Context) (bool, error) {
return r.queries.HasSkipSegmentOverrideTable(ctx)
}

View File

@@ -272,7 +272,7 @@ func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, title
}
// Final assembly
segments := s.fetchSkipSegments(ctx, animeID, episode)
segments := s.fetchSkipSegments(ctx, userID, animeID, episode)
watchData := map[string]any{
"MalID": animeID,
@@ -351,7 +351,41 @@ func (s *playbackService) SaveProgress(ctx context.Context, userID string, anime
return err
}
func (s *playbackService) fetchSkipSegments(ctx context.Context, malID int, episode string) []SkipSegment {
func (s *playbackService) UpsertSkipSegmentOverride(ctx context.Context, userID string, animeID int64, episode int, skipType string, startTime, endTime float64) error {
if userID == "" {
return fmt.Errorf("not authenticated")
}
if animeID <= 0 || episode <= 0 {
return fmt.Errorf("invalid anime/episode")
}
t := strings.ToLower(strings.TrimSpace(skipType))
switch t {
case "op", "opening", "intro":
t = "op"
case "ed", "ending", "outro":
t = "ed"
default:
return fmt.Errorf("invalid skip_type")
}
if !(startTime >= 0) || !(endTime > startTime) {
return fmt.Errorf("invalid interval")
}
// let the player-side filters ignore obviously wrong durations, but keep some sanity.
if endTime-startTime < 5 || endTime-startTime > 10*60 {
return fmt.Errorf("interval duration out of range")
}
return s.repo.UpsertSkipSegmentOverride(ctx, db.SkipSegmentOverride{
ID: uuid.New().String(),
UserID: userID,
AnimeID: animeID,
Episode: int64(episode),
SkipType: t,
StartTime: startTime,
EndTime: endTime,
})
}
func (s *playbackService) fetchSkipSegments(ctx context.Context, userID string, malID int, episode string) []SkipSegment {
if malID <= 0 || strings.TrimSpace(episode) == "" {
return []SkipSegment{}
}
@@ -415,6 +449,47 @@ func (s *playbackService) fetchSkipSegments(ctx context.Context, malID int, epis
})
}
epNum, _ := strconv.ParseInt(strings.TrimSpace(episode), 10, 64)
if userID != "" && epNum > 0 {
if ok, err := s.repo.HasSkipSegmentOverrideTable(ctx); err == nil && ok {
if overrides, err := s.repo.ListSkipSegmentOverrides(ctx, userID, int64(malID), epNum); err == nil {
// Build map keyed by normalized type ("opening"/"ending")
overrideByType := make(map[string]SkipSegment, len(overrides))
for _, o := range overrides {
t := strings.ToLower(strings.TrimSpace(o.SkipType))
switch t {
case "op", "opening", "intro":
t = "opening"
case "ed", "ending", "outro":
t = "ending"
default:
continue
}
overrideByType[t] = SkipSegment{Type: t, Start: o.StartTime, End: o.EndTime}
}
if len(overrideByType) > 0 {
merged := make([]SkipSegment, 0, len(segments)+len(overrideByType))
seen := map[string]bool{}
for _, seg := range segments {
if o, ok := overrideByType[seg.Type]; ok {
merged = append(merged, o)
seen[seg.Type] = true
} else {
merged = append(merged, seg)
seen[seg.Type] = true
}
}
for t, o := range overrideByType {
if !seen[t] {
merged = append(merged, o)
}
}
segments = merged
}
}
}
}
return segments
}