From 51355a4dbc67ee1908460a6b2e39184b968694e0 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Fri, 22 May 2026 16:34:26 +0200 Subject: [PATCH] fix: render segment overrides and skip progress --- internal/domain/playback.go | 7 +- internal/playback/handler/handler.go | 5 ++ internal/playback/service/service.go | 100 ++++++++++++--------------- static/player/progress.ts | 5 ++ static/player/skip/editor.ts | 8 ++- static/player/skip/segments.ts | 4 ++ static/player/types.ts | 2 + 7 files changed, 73 insertions(+), 58 deletions(-) diff --git a/internal/domain/playback.go b/internal/domain/playback.go index 81bad68..0e1953e 100644 --- a/internal/domain/playback.go +++ b/internal/domain/playback.go @@ -63,9 +63,10 @@ type SeasonEntry struct { } type SkipSegment struct { - Type string `json:"type"` - Start float64 `json:"start"` - End float64 `json:"end"` + Type string `json:"type"` + Start float64 `json:"start"` + End float64 `json:"end"` + Source string `json:"source,omitempty"` } type ProviderStream struct { diff --git a/internal/playback/handler/handler.go b/internal/playback/handler/handler.go index c399a57..9cd1483 100644 --- a/internal/playback/handler/handler.go +++ b/internal/playback/handler/handler.go @@ -146,6 +146,11 @@ func (h *PlaybackHandler) HandleSaveProgress(c *gin.Context) { if u, ok := user.(*domain.User); ok { userID = u.ID } + if userID == "" { + // Avoid spamming 500s for anonymous playback; progress is user-scoped. + c.Status(http.StatusUnauthorized) + return + } var req struct { MalID int64 `json:"mal_id"` diff --git a/internal/playback/service/service.go b/internal/playback/service/service.go index 27fa981..3f0e134 100644 --- a/internal/playback/service/service.go +++ b/internal/playback/service/service.go @@ -363,63 +363,50 @@ func (s *playbackService) fetchSkipSegments(ctx context.Context, userID string, return []domain.SkipSegment{} } + segments := []domain.SkipSegment{} + endpoint := fmt.Sprintf("https://api.aniskip.com/v1/skip-times/%s/%s?types=op&types=ed", url.PathEscape(strconv.Itoa(malID)), url.PathEscape(episode)) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return []domain.SkipSegment{} - } - req.Header.Set("User-Agent", useragent.Generic) + if err == nil { + req.Header.Set("User-Agent", useragent.Generic) + if resp, err := s.httpClient.Do(req); err == nil { + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode == http.StatusOK { + if body, err := io.ReadAll(io.LimitReader(resp.Body, limits.KiB512)); err == nil { + type resultItem struct { + SkipType string `json:"skip_type"` + Interval struct { + StartTime float64 `json:"start_time"` + EndTime float64 `json:"end_time"` + } `json:"interval"` + } + type apiResponse struct { + Found bool `json:"found"` + Result []resultItem `json:"results"` + } - resp, err := s.httpClient.Do(req) - if err != nil { - return []domain.SkipSegment{} - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - return []domain.SkipSegment{} - } - - body, err := io.ReadAll(io.LimitReader(resp.Body, limits.KiB512)) - if err != nil { - return []domain.SkipSegment{} - } - - type resultItem struct { - SkipType string `json:"skip_type"` - Interval struct { - StartTime float64 `json:"start_time"` - EndTime float64 `json:"end_time"` - } `json:"interval"` - } - type apiResponse struct { - Found bool `json:"found"` - Result []resultItem `json:"results"` - } - - var parsed apiResponse - if err := json.Unmarshal(body, &parsed); err != nil { - return []domain.SkipSegment{} - } - - if !parsed.Found || len(parsed.Result) == 0 { - return []domain.SkipSegment{} - } - - segments := make([]domain.SkipSegment, 0, len(parsed.Result)) - for _, r := range parsed.Result { - skipType := strings.ToLower(r.SkipType) - switch skipType { - case "op": - skipType = "opening" - case "ed": - skipType = "ending" + var parsed apiResponse + if err := json.Unmarshal(body, &parsed); err == nil && parsed.Found && len(parsed.Result) > 0 { + segments = make([]domain.SkipSegment, 0, len(parsed.Result)) + for _, r := range parsed.Result { + skipType := strings.ToLower(r.SkipType) + switch skipType { + case "op": + skipType = "opening" + case "ed": + skipType = "ending" + } + segments = append(segments, domain.SkipSegment{ + Type: skipType, + Start: r.Interval.StartTime, + End: r.Interval.EndTime, + Source: "aniskip", + }) + } + } + } + } } - segments = append(segments, domain.SkipSegment{ - Type: skipType, - Start: r.Interval.StartTime, - End: r.Interval.EndTime, - }) } epNum, _ := strconv.ParseInt(strings.TrimSpace(episode), 10, 64) @@ -438,7 +425,12 @@ func (s *playbackService) fetchSkipSegments(ctx context.Context, userID string, default: continue } - overrideByType[t] = domain.SkipSegment{Type: t, Start: o.StartTime, End: o.EndTime} + overrideByType[t] = domain.SkipSegment{ + Type: t, + Start: o.StartTime, + End: o.EndTime, + Source: "override", + } } if len(overrideByType) > 0 { merged := make([]domain.SkipSegment, 0, len(segments)+len(overrideByType)) diff --git a/static/player/progress.ts b/static/player/progress.ts index d5d4a30..7e478db 100644 --- a/static/player/progress.ts +++ b/static/player/progress.ts @@ -22,6 +22,8 @@ const sendBeacon = (payload: string) => { */ export const saveProgress = async (): Promise => { if (state.transitionEpisode !== null || !state.malID || state.video.currentTime < 1) return; + // progress is user-scoped; avoid spamming 401s for anonymous sessions + if (!document.cookie.includes('mal_session=')) return; const episode = Number.parseInt(state.currentEpisode, 10); if (!episode) return; @@ -60,6 +62,8 @@ const scheduleProgressSave = (): void => { */ export const markEpisodeTransition = (episodeNumber: number): void => { if (!state.malID || !episodeNumber) return; + // progress is user-scoped; avoid sending beacons for anonymous sessions + if (!document.cookie.includes('mal_session=')) return; if (state.progressSaveTimer !== undefined) { window.clearTimeout(state.progressSaveTimer); state.progressSaveTimer = undefined; @@ -102,6 +106,7 @@ export const setupProgress = (): void => { // save on page close window.addEventListener('beforeunload', () => { if (state.transitionEpisode !== null || state.completionSent || !state.malID) return; + if (!document.cookie.includes('mal_session=')) return; const episode = Number.parseInt(state.currentEpisode, 10); if (!episode) return; sendBeacon(buildPayload(episode, displayTimeFromAbsolute(state.video.currentTime))); diff --git a/static/player/skip/editor.ts b/static/player/skip/editor.ts index a827b9c..15d62e6 100644 --- a/static/player/skip/editor.ts +++ b/static/player/skip/editor.ts @@ -151,10 +151,16 @@ export const setupSegmentEditor = (): void => { if (normalizedType === 'ending') return t !== 'ed' && t !== 'ending' && t !== 'outro'; return t !== 'op' && t !== 'opening' && t !== 'intro'; }); - state.parsedSegments.push({ type: normalizedType, start: startTime, end: endTime }); + state.parsedSegments.push({ + type: normalizedType, + start: startTime, + end: endTime, + source: 'override', + }); resolveActiveSegments(); renderSegments(); + window.showToast?.({ message: 'Segment saved.' }); close(); } catch { setError('Failed to save segment.'); diff --git a/static/player/skip/segments.ts b/static/player/skip/segments.ts index 7606d7f..693b12c 100644 --- a/static/player/skip/segments.ts +++ b/static/player/skip/segments.ts @@ -27,6 +27,7 @@ export const resolveActiveSegments = (): void => { state.activeSegments = state.parsedSegments.filter(s => { const t = normalizeType(s.type); if (!t) return false; + const isOverride = (s.source || '').toLowerCase() === 'override'; const len = s.end - s.start; // duration filter @@ -34,6 +35,9 @@ export const resolveActiveSegments = (): void => { // bounds check if (s.start < 0 || s.end <= s.start || s.end > bounds + 1) return false; + // User overrides should render even if they don't fit AniSkip's usual OP/ED heuristics. + if (isOverride) return true; + // intro: starts early, before 50% of video if (t === 'op') { return s.start <= MAX_INTRO_START && s.start <= bounds * 0.5; diff --git a/static/player/types.ts b/static/player/types.ts index a264097..2baaf92 100644 --- a/static/player/types.ts +++ b/static/player/types.ts @@ -16,6 +16,7 @@ export interface SkipSegment { type: string; // 'op' or 'ed' start: number; end: number; + source?: string; } // parsed subtitle cue from VTT @@ -37,6 +38,7 @@ export interface ActiveSegment { type: string; start: number; end: number; + source?: string; } // timeline range (handles seekable ranges in live streams)