feat: typed anime and playback payloads

This commit is contained in:
2026-05-21 15:07:09 +02:00
parent d94f1516ce
commit 812dcd2448
7 changed files with 181 additions and 135 deletions

View File

@@ -101,14 +101,11 @@ func (h *AnimeHandler) renderCatalogSection(c *gin.Context, section string) {
return return
} }
watchlistMap := map[int64]bool{} watchlistMap := h.watchlistMapForAnimes(c.Request.Context(), userID, data.Animes)
if animes, ok := data["Animes"].([]domain.Anime); ok {
watchlistMap = h.watchlistMapForAnimes(c.Request.Context(), userID, animes)
}
data["Section"] = section data.Section = section
data["_fragment"] = "catalog_section" data.Fragment = "catalog_section"
data["WatchlistMap"] = watchlistMap data.WatchlistMap = watchlistMap
c.HTML(http.StatusOK, "index.gohtml", data) c.HTML(http.StatusOK, "index.gohtml", data)
} }
@@ -143,14 +140,11 @@ func (h *AnimeHandler) renderDiscoverSection(c *gin.Context, section string) {
return return
} }
watchlistMap := map[int64]bool{} watchlistMap := h.watchlistMapForAnimes(c.Request.Context(), userID, data.Animes)
if animes, ok := data["Animes"].([]domain.Anime); ok {
watchlistMap = h.watchlistMapForAnimes(c.Request.Context(), userID, animes)
}
data["Section"] = section data.Section = section
data["_fragment"] = "discover_section" data.Fragment = "discover_section"
data["WatchlistMap"] = watchlistMap data.WatchlistMap = watchlistMap
c.HTML(http.StatusOK, "discover.gohtml", data) c.HTML(http.StatusOK, "discover.gohtml", data)
} }

View File

@@ -20,7 +20,7 @@ func NewAnimeService(jikan *jikan.Client, repo domain.AnimeRepository) domain.An
return &animeService{jikan: jikan, repo: repo} return &animeService{jikan: jikan, repo: repo}
} }
func (s *animeService) GetCatalogSection(ctx context.Context, userID string, section string) (map[string]any, error) { func (s *animeService) GetCatalogSection(ctx context.Context, userID string, section string) (domain.CatalogSectionData, error) {
var ( var (
res jikan.TopAnimeResult res jikan.TopAnimeResult
cw []db.GetContinueWatchingEntriesRow cw []db.GetContinueWatchingEntriesRow
@@ -48,7 +48,7 @@ func (s *animeService) GetCatalogSection(ctx context.Context, userID string, sec
} }
if err := g.Wait(); err != nil { if err := g.Wait(); err != nil {
return nil, err return domain.CatalogSectionData{}, err
} }
animes := res.Animes animes := res.Animes
@@ -56,13 +56,13 @@ func (s *animeService) GetCatalogSection(ctx context.Context, userID string, sec
animes = animes[:6] animes = animes[:6]
} }
return map[string]any{ return domain.CatalogSectionData{
"Animes": animes, Animes: animes,
"ContinueWatching": cw, ContinueWatching: cw,
}, nil }, nil
} }
func (s *animeService) GetDiscoverSection(ctx context.Context, userID string, section string) (map[string]any, error) { func (s *animeService) GetDiscoverSection(ctx context.Context, userID string, section string) (domain.DiscoverSectionData, error) {
var res jikan.TopAnimeResult var res jikan.TopAnimeResult
g, gCtx := errgroup.WithContext(ctx) g, gCtx := errgroup.WithContext(ctx)
@@ -81,7 +81,7 @@ func (s *animeService) GetDiscoverSection(ctx context.Context, userID string, se
}) })
if err := g.Wait(); err != nil { if err := g.Wait(); err != nil {
return nil, err return domain.DiscoverSectionData{}, err
} }
animes := res.Animes animes := res.Animes
@@ -89,8 +89,8 @@ func (s *animeService) GetDiscoverSection(ctx context.Context, userID string, se
animes = animes[:8] animes = animes[:8]
} }
return map[string]any{ return domain.DiscoverSectionData{
"Animes": animes, Animes: animes,
}, nil }, nil
} }

View File

@@ -17,8 +17,8 @@ type ThemesData = jikan.ThemesData
type ReviewEntry = jikan.ReviewEntry type ReviewEntry = jikan.ReviewEntry
type AnimeService interface { type AnimeService interface {
GetCatalogSection(ctx context.Context, userID string, section string) (map[string]any, error) GetCatalogSection(ctx context.Context, userID string, section string) (CatalogSectionData, error)
GetDiscoverSection(ctx context.Context, userID string, section string) (map[string]any, error) GetDiscoverSection(ctx context.Context, userID string, section string) (DiscoverSectionData, error)
GetAnimeByID(ctx context.Context, id int) (Anime, error) GetAnimeByID(ctx context.Context, id int) (Anime, error)
SearchAdvanced(ctx context.Context, q, animeType, status, orderBy, sort string, genres []int, sfw bool, page, limit int) (jikan.SearchResult, error) SearchAdvanced(ctx context.Context, q, animeType, status, orderBy, sort string, genres []int, sfw bool, page, limit int) (jikan.SearchResult, error)
GetGenres(ctx context.Context) ([]Genre, error) GetGenres(ctx context.Context) ([]Genre, error)
@@ -34,6 +34,29 @@ type AnimeService interface {
GetReviews(ctx context.Context, id int, page int) ([]ReviewEntry, bool, error) GetReviews(ctx context.Context, id int, page int) ([]ReviewEntry, bool, error)
} }
type CatalogSectionData struct {
Animes []Anime
ContinueWatching []db.GetContinueWatchingEntriesRow
Section string
WatchlistMap map[int64]bool
Fragment string
}
func (d CatalogSectionData) TemplateFragment() string {
return d.Fragment
}
type DiscoverSectionData struct {
Animes []Anime
Section string
WatchlistMap map[int64]bool
Fragment string
}
func (d DiscoverSectionData) TemplateFragment() string {
return d.Fragment
}
type AnimeRepository interface { type AnimeRepository interface {
GetUserWatchList(ctx context.Context, userID string) ([]db.GetUserWatchListRow, error) GetUserWatchList(ctx context.Context, userID string) ([]db.GetUserWatchListRow, error)
GetWatchListEntry(ctx context.Context, params db.GetWatchListEntryParams) (db.WatchListEntry, error) GetWatchListEntry(ctx context.Context, params db.GetWatchListEntryParams) (db.WatchListEntry, error)

View File

@@ -6,13 +6,68 @@ 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) (WatchPageData, 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 CompleteAnime(ctx context.Context, userID string, animeID int64) error
ResolveProxyToken(token string) (string, string, error) ResolveProxyToken(token string) (string, string, error)
UpsertSkipSegmentOverride(ctx context.Context, userID string, animeID int64, episode int, skipType string, startTime, endTime float64) error UpsertSkipSegmentOverride(ctx context.Context, userID string, animeID int64, episode int, skipType string, startTime, endTime float64) error
} }
type WatchPageData struct {
WatchData WatchData
Anime Anime
Episodes []CanonicalEpisode
CurrentEpID string
WatchlistStatus string
WatchlistIDs []int64
Seasons []SeasonEntry
User *User
CurrentPath string
Error string
}
type WatchData struct {
MalID int
Title string
CurrentEpisode string
StartTimeSeconds float64
Episodes []CanonicalEpisode
Providers []ProviderData
ModeSources map[string]ModeSource
InitialMode string
ModeSwitchedFrom string
AvailableModes []string
Segments []SkipSegment
}
type SubtitleItem struct {
Lang string `json:"lang"`
URL string `json:"url,omitempty"`
Referer string `json:"referer,omitempty"`
Token string `json:"token"`
}
type ModeSource struct {
URL string `json:"url,omitempty"`
Referer string `json:"referer,omitempty"`
Token string `json:"token"`
Subtitles []SubtitleItem `json:"subtitles"`
Qualities []string `json:"qualities,omitempty"`
}
type SeasonEntry struct {
MalID int `json:"mal_id"`
Title string `json:"title"`
Prefix string `json:"prefix"`
IsCurrent bool `json:"is_current"`
}
type SkipSegment struct {
Type string `json:"type"`
Start float64 `json:"start"`
End float64 `json:"end"`
}
type ProviderStream struct { type ProviderStream struct {
Name string `json:"name"` Name string `json:"name"`
URL string `json:"url"` URL string `json:"url"`

View File

@@ -7,7 +7,6 @@ import (
"mal/pkg/net/limits" "mal/pkg/net/limits"
"mal/pkg/net/proxytransport" "mal/pkg/net/proxytransport"
"mal/pkg/net/useragent" "mal/pkg/net/useragent"
"maps"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@@ -61,26 +60,25 @@ func (h *PlaybackHandler) HandleWatchPage(c *gin.Context) {
data, err := h.svc.BuildWatchData(c.Request.Context(), id, []string{}, ep, mode, userID) data, err := h.svc.BuildWatchData(c.Request.Context(), id, []string{}, ep, mode, userID)
if err != nil { if err != nil {
anime, _ := h.animeSvc.GetAnimeByID(c.Request.Context(), id) anime, _ := h.animeSvc.GetAnimeByID(c.Request.Context(), id)
c.HTML(http.StatusOK, "watch.gohtml", gin.H{ c.HTML(http.StatusOK, "watch.gohtml", domain.WatchPageData{
"Error": err.Error(), Error: err.Error(),
"Anime": anime, Anime: anime,
"Episodes": []domain.EpisodeData{}, Episodes: []domain.CanonicalEpisode{},
"CurrentPath": c.Request.URL.Path, CurrentPath: c.Request.URL.Path,
"User": user, User: currentUser(user),
"CurrentEpID": ep, CurrentEpID: ep,
"WatchData": map[string]any{"Episodes": []domain.EpisodeData{}, "Providers": []any{}}, WatchData: domain.WatchData{
Episodes: []domain.CanonicalEpisode{},
Providers: []domain.ProviderData{},
},
}) })
return return
} }
// Merge data from service with handler-specific context data.User = currentUser(user)
responseData := gin.H{ data.CurrentPath = c.Request.URL.Path
"User": user,
"CurrentPath": c.Request.URL.Path,
}
maps.Copy(responseData, data)
c.HTML(http.StatusOK, "watch.gohtml", responseData) c.HTML(http.StatusOK, "watch.gohtml", data)
} }
// HandleEpisodeData returns the minimal payload needed to advance to the next // HandleEpisodeData returns the minimal payload needed to advance to the next
@@ -112,39 +110,36 @@ func (h *PlaybackHandler) HandleEpisodeData(c *gin.Context) {
return return
} }
watchData, _ := data["WatchData"].(map[string]any) watchData := data.WatchData
if watchData == nil {
c.Status(http.StatusInternalServerError)
return
}
modeSources := watchData["ModeSources"]
availableModes, _ := watchData["AvailableModes"].([]string)
segments := watchData["Segments"]
// Try to resolve a title for this episode from the episode list. // Try to resolve a title for this episode from the episode list.
episodeTitle := "" episodeTitle := ""
if eps, ok := watchData["Episodes"].([]domain.CanonicalEpisode); ok { epNum, _ := strconv.Atoi(episode)
epNum, _ := strconv.Atoi(episode) for _, e := range watchData.Episodes {
for _, e := range eps { if e.Number == epNum {
if e.Number == epNum { episodeTitle = e.Title
episodeTitle = e.Title break
break
}
} }
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"mode_sources": modeSources, "mode_sources": watchData.ModeSources,
"available_modes": availableModes, "available_modes": watchData.AvailableModes,
"initial_mode": watchData["InitialMode"], "initial_mode": watchData.InitialMode,
"start_time_seconds": watchData["StartTimeSeconds"], "start_time_seconds": watchData.StartTimeSeconds,
"segments": segments, "segments": watchData.Segments,
"episode_title": episodeTitle, "episode_title": episodeTitle,
"mode_switched_from": watchData["ModeSwitchedFrom"], "mode_switched_from": watchData.ModeSwitchedFrom,
}) })
} }
func currentUser(value any) *domain.User {
if user, ok := value.(*domain.User); ok {
return user
}
return nil
}
func (h *PlaybackHandler) HandleSaveProgress(c *gin.Context) { func (h *PlaybackHandler) HandleSaveProgress(c *gin.Context) {
user, _ := c.Get("User") user, _ := c.Get("User")
userID := "" userID := ""

View File

@@ -33,12 +33,6 @@ type playbackService struct {
proxyTokenKey string proxyTokenKey string
} }
type SkipSegment struct {
Type string `json:"type"`
Start float64 `json:"start"`
End float64 `json:"end"`
}
type proxyTokenPayload struct { type proxyTokenPayload struct {
TargetURL string `json:"u"` TargetURL string `json:"u"`
Referer string `json:"r,omitempty"` Referer string `json:"r,omitempty"`
@@ -109,11 +103,11 @@ func (s *playbackService) ResolveProxyToken(token string) (string, string, error
return payload.TargetURL, payload.Referer, nil return payload.TargetURL, payload.Referer, nil
} }
func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, titleCandidates []string, episode string, mode string, userID string) (map[string]any, error) { func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, titleCandidates []string, episode string, mode string, userID string) (domain.WatchPageData, error) {
// 1. Get Anime details for total episodes and titles // 1. Get Anime details for total episodes and titles
anime, err := s.jikan.GetAnimeByID(ctx, animeID) anime, err := s.jikan.GetAnimeByID(ctx, animeID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch anime: %w", err) return domain.WatchPageData{}, fmt.Errorf("failed to fetch anime: %w", err)
} }
// 2. Resolve streams from providers // 2. Resolve streams from providers
@@ -132,7 +126,7 @@ func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, title
canonicalEpisodes, err := s.episodes.GetCanonicalEpisodes(ctx, anime, false) canonicalEpisodes, err := s.episodes.GetCanonicalEpisodes(ctx, anime, false)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch episodes: %w", err) return domain.WatchPageData{}, fmt.Errorf("failed to fetch episodes: %w", err)
} }
requestedMode := mode requestedMode := mode
@@ -147,22 +141,7 @@ func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, title
} }
} }
type SubtitleItem struct { modeSources := map[string]domain.ModeSource{}
Lang string `json:"lang"`
URL string `json:"url,omitempty"`
Referer string `json:"referer,omitempty"`
Token string `json:"token"`
}
type ModeSource struct {
URL string `json:"url,omitempty"`
Referer string `json:"referer,omitempty"`
Token string `json:"token"`
Subtitles []SubtitleItem `json:"subtitles"`
Qualities []string `json:"qualities,omitempty"`
}
modeSources := map[string]ModeSource{}
var result *domain.StreamResult var result *domain.StreamResult
for _, m := range []string{"sub", "dub"} { for _, m := range []string{"sub", "dub"} {
@@ -172,17 +151,17 @@ func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, title
continue continue
} }
var subItems []SubtitleItem var subItems []domain.SubtitleItem
for _, sub := range res.Subtitles { for _, sub := range res.Subtitles {
subToken, _ := s.SignProxyToken(sub.URL, res.Referer, "subtitle") subToken, _ := s.SignProxyToken(sub.URL, res.Referer, "subtitle")
subItems = append(subItems, SubtitleItem{ subItems = append(subItems, domain.SubtitleItem{
Lang: sub.Label, Lang: sub.Label,
Token: subToken, Token: subToken,
}) })
} }
streamToken, _ := s.SignProxyToken(res.URL, res.Referer, "stream") streamToken, _ := s.SignProxyToken(res.URL, res.Referer, "stream")
modeSources[m] = ModeSource{ modeSources[m] = domain.ModeSource{
URL: res.URL, URL: res.URL,
Referer: res.Referer, Referer: res.Referer,
Token: streamToken, Token: streamToken,
@@ -197,11 +176,11 @@ func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, title
} }
if len(modeSources) == 0 { if len(modeSources) == 0 {
return nil, fmt.Errorf("no streams found") return domain.WatchPageData{}, fmt.Errorf("no streams found")
} }
if result == nil { if result == nil {
return nil, fmt.Errorf("no streams found for mode %s", mode) return domain.WatchPageData{}, fmt.Errorf("no streams found for mode %s", mode)
} }
// 3. Get start time from progress // 3. Get start time from progress
@@ -248,17 +227,11 @@ func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, title
// 6. Resolve relations/seasons // 6. Resolve relations/seasons
relations, _ := s.jikan.GetFullRelations(ctx, animeID) relations, _ := s.jikan.GetFullRelations(ctx, animeID)
type SeasonEntry struct { var seasons []domain.SeasonEntry
MalID int `json:"mal_id"`
Title string `json:"title"`
Prefix string `json:"prefix"`
IsCurrent bool `json:"is_current"`
}
var seasons []SeasonEntry
tvCounter := 1 tvCounter := 1
for _, rel := range relations { for _, rel := range relations {
if strings.ToLower(rel.Anime.Type) == "tv" || strings.ToLower(rel.Anime.Type) == "movie" { if strings.ToLower(rel.Anime.Type) == "tv" || strings.ToLower(rel.Anime.Type) == "movie" {
seasons = append(seasons, SeasonEntry{ seasons = append(seasons, domain.SeasonEntry{
MalID: rel.Anime.MalID, MalID: rel.Anime.MalID,
Title: rel.Anime.DisplayTitle(), Title: rel.Anime.DisplayTitle(),
Prefix: rel.Relation, Prefix: rel.Relation,
@@ -274,19 +247,19 @@ func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, title
// Final assembly // Final assembly
segments := s.fetchSkipSegments(ctx, userID, animeID, episode) segments := s.fetchSkipSegments(ctx, userID, animeID, episode)
watchData := map[string]any{ watchData := domain.WatchData{
"MalID": animeID, MalID: animeID,
"Title": anime.DisplayTitle(), Title: anime.DisplayTitle(),
"CurrentEpisode": episode, CurrentEpisode: episode,
"StartTimeSeconds": startTime, StartTimeSeconds: startTime,
"Episodes": canonicalEpisodes.Episodes, Episodes: canonicalEpisodes.Episodes,
"Providers": []domain.ProviderData{ Providers: []domain.ProviderData{
{Streams: streams}, {Streams: streams},
}, },
"ModeSources": modeSources, ModeSources: modeSources,
"InitialMode": mode, InitialMode: mode,
"ModeSwitchedFrom": modeSwitchedFrom, ModeSwitchedFrom: modeSwitchedFrom,
"AvailableModes": func() []string { AvailableModes: func() []string {
var modes []string var modes []string
for m := range modeSources { for m := range modeSources {
modes = append(modes, m) modes = append(modes, m)
@@ -294,17 +267,17 @@ func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, title
sort.Strings(modes) sort.Strings(modes)
return modes return modes
}(), }(),
"Segments": segments, Segments: segments,
} }
return map[string]any{ return domain.WatchPageData{
"WatchData": watchData, WatchData: watchData,
"Anime": anime, Anime: anime,
"Episodes": canonicalEpisodes.Episodes, Episodes: canonicalEpisodes.Episodes,
"CurrentEpID": episode, CurrentEpID: episode,
"WatchlistStatus": watchlistStatus, WatchlistStatus: watchlistStatus,
"WatchlistIDs": watchlistIDs, WatchlistIDs: watchlistIDs,
"Seasons": seasons, Seasons: seasons,
}, nil }, nil
} }
@@ -385,31 +358,31 @@ func (s *playbackService) UpsertSkipSegmentOverride(ctx context.Context, userID
}) })
} }
func (s *playbackService) fetchSkipSegments(ctx context.Context, userID string, malID int, episode string) []SkipSegment { func (s *playbackService) fetchSkipSegments(ctx context.Context, userID string, malID int, episode string) []domain.SkipSegment {
if malID <= 0 || strings.TrimSpace(episode) == "" { if malID <= 0 || strings.TrimSpace(episode) == "" {
return []SkipSegment{} return []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)) 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) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil { if err != nil {
return []SkipSegment{} return []domain.SkipSegment{}
} }
req.Header.Set("User-Agent", useragent.Generic) req.Header.Set("User-Agent", useragent.Generic)
resp, err := s.httpClient.Do(req) resp, err := s.httpClient.Do(req)
if err != nil { if err != nil {
return []SkipSegment{} return []domain.SkipSegment{}
} }
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return []SkipSegment{} return []domain.SkipSegment{}
} }
body, err := io.ReadAll(io.LimitReader(resp.Body, limits.KiB512)) body, err := io.ReadAll(io.LimitReader(resp.Body, limits.KiB512))
if err != nil { if err != nil {
return []SkipSegment{} return []domain.SkipSegment{}
} }
type resultItem struct { type resultItem struct {
@@ -426,14 +399,14 @@ func (s *playbackService) fetchSkipSegments(ctx context.Context, userID string,
var parsed apiResponse var parsed apiResponse
if err := json.Unmarshal(body, &parsed); err != nil { if err := json.Unmarshal(body, &parsed); err != nil {
return []SkipSegment{} return []domain.SkipSegment{}
} }
if !parsed.Found || len(parsed.Result) == 0 { if !parsed.Found || len(parsed.Result) == 0 {
return []SkipSegment{} return []domain.SkipSegment{}
} }
segments := make([]SkipSegment, 0, len(parsed.Result)) segments := make([]domain.SkipSegment, 0, len(parsed.Result))
for _, r := range parsed.Result { for _, r := range parsed.Result {
skipType := strings.ToLower(r.SkipType) skipType := strings.ToLower(r.SkipType)
switch skipType { switch skipType {
@@ -442,7 +415,7 @@ func (s *playbackService) fetchSkipSegments(ctx context.Context, userID string,
case "ed": case "ed":
skipType = "ending" skipType = "ending"
} }
segments = append(segments, SkipSegment{ segments = append(segments, domain.SkipSegment{
Type: skipType, Type: skipType,
Start: r.Interval.StartTime, Start: r.Interval.StartTime,
End: r.Interval.EndTime, End: r.Interval.EndTime,
@@ -454,7 +427,7 @@ func (s *playbackService) fetchSkipSegments(ctx context.Context, userID string,
if ok, err := s.repo.HasSkipSegmentOverrideTable(ctx); err == nil && ok { if ok, err := s.repo.HasSkipSegmentOverrideTable(ctx); err == nil && ok {
if overrides, err := s.repo.ListSkipSegmentOverrides(ctx, userID, int64(malID), epNum); err == nil { if overrides, err := s.repo.ListSkipSegmentOverrides(ctx, userID, int64(malID), epNum); err == nil {
// Build map keyed by normalized type ("opening"/"ending") // Build map keyed by normalized type ("opening"/"ending")
overrideByType := make(map[string]SkipSegment, len(overrides)) overrideByType := make(map[string]domain.SkipSegment, len(overrides))
for _, o := range overrides { for _, o := range overrides {
t := strings.ToLower(strings.TrimSpace(o.SkipType)) t := strings.ToLower(strings.TrimSpace(o.SkipType))
switch t { switch t {
@@ -465,10 +438,10 @@ func (s *playbackService) fetchSkipSegments(ctx context.Context, userID string,
default: default:
continue continue
} }
overrideByType[t] = SkipSegment{Type: t, Start: o.StartTime, End: o.EndTime} overrideByType[t] = domain.SkipSegment{Type: t, Start: o.StartTime, End: o.EndTime}
} }
if len(overrideByType) > 0 { if len(overrideByType) > 0 {
merged := make([]SkipSegment, 0, len(segments)+len(overrideByType)) merged := make([]domain.SkipSegment, 0, len(segments)+len(overrideByType))
seen := map[string]bool{} seen := map[string]bool{}
for _, seg := range segments { for _, seg := range segments {
if o, ok := overrideByType[seg.Type]; ok { if o, ok := overrideByType[seg.Type]; ok {

View File

@@ -205,6 +205,10 @@ type HTMLRender struct {
Data any Data any
} }
type templateFragmentData interface {
TemplateFragment() string
}
func (h HTMLRender) Render(w http.ResponseWriter) error { func (h HTMLRender) Render(w http.ResponseWriter) error {
tmpl, ok := h.Renderer.templates[h.Name] tmpl, ok := h.Renderer.templates[h.Name]
if !ok { if !ok {
@@ -219,6 +223,8 @@ func (h HTMLRender) Render(w http.ResponseWriter) error {
block = dataMap["_fragment"] block = dataMap["_fragment"]
} else if ginH, ok := h.Data.(gin.H); ok { } else if ginH, ok := h.Data.(gin.H); ok {
block = ginH["_fragment"] block = ginH["_fragment"]
} else if fragmentData, ok := h.Data.(templateFragmentData); ok {
block = fragmentData.TemplateFragment()
} }
if blockStr, ok := block.(string); ok && blockStr != "" { if blockStr, ok := block.(string); ok && blockStr != "" {