feat: typed anime and playback payloads
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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 := ""
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 != "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user