Files
silo-server/internal/api/handlers/calendar.go
T

202 lines
5.6 KiB
Go

package handlers
import (
"context"
"net/http"
"strconv"
"time"
"github.com/Silo-Server/silo-server/internal/catalog"
)
type calendarRepository interface {
ListEvents(ctx context.Context, f catalog.CalendarFilter) ([]catalog.CalendarEvent, error)
}
// CalendarHandler handles the calendar endpoint.
type CalendarHandler struct {
repo calendarRepository
detailSvc *catalog.DetailService
}
// NewCalendarHandler creates a new CalendarHandler.
func NewCalendarHandler(repo *catalog.CalendarRepository, detailSvc *catalog.DetailService) *CalendarHandler {
return &CalendarHandler{repo: repo, detailSvc: detailSvc}
}
// --- Response types ---
type calendarEventResponse struct {
ContentID string `json:"content_id"`
Type string `json:"type"`
Title string `json:"title"`
EpisodeTitle *string `json:"episode_title,omitempty"`
SeriesID *string `json:"series_id,omitempty"`
SeasonNumber *int `json:"season_number,omitempty"`
EpisodeNumber *int `json:"episode_number,omitempty"`
AirDate string `json:"air_date"`
AirTime *string `json:"air_time,omitempty"`
PosterURL string `json:"poster_url,omitempty"`
PosterThumbhash string `json:"poster_thumbhash,omitempty"`
Badges []string `json:"badges"`
}
type calendarDayResponse struct {
Date string `json:"date"`
Items []calendarEventResponse `json:"items"`
}
type calendarResponse struct {
Events []calendarDayResponse `json:"events"`
}
// HandleGetCalendar returns calendar events for a date range.
func (h *CalendarHandler) HandleGetCalendar(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
start, err := time.Parse("2006-01-02", q.Get("start"))
if err != nil {
writeError(w, http.StatusBadRequest, "bad_request", "start must be a valid date (YYYY-MM-DD)")
return
}
end, err := time.Parse("2006-01-02", q.Get("end"))
if err != nil {
writeError(w, http.StatusBadRequest, "bad_request", "end must be a valid date (YYYY-MM-DD)")
return
}
if end.Before(start) {
writeError(w, http.StatusBadRequest, "bad_request", "end must not be before start")
return
}
if end.Sub(start) > 30*24*time.Hour {
writeError(w, http.StatusBadRequest, "bad_request", "date range cannot exceed 31 days")
return
}
filter := q.Get("filter")
if filter == "" {
filter = "all"
}
switch filter {
case "all", "favorites", "watchlist":
default:
writeError(w, http.StatusBadRequest, "bad_request", "filter must be all, favorites, or watchlist")
return
}
af := requestAccessFilter(r)
if (filter == "favorites" || filter == "watchlist") && af.ProfileID == "" {
writeError(w, http.StatusBadRequest, "bad_request", "profile required for favorites/watchlist filter")
return
}
cf := catalog.CalendarFilter{
Start: start,
End: end,
Filter: filter,
AllowedLibraryIDs: af.AllowedLibraryIDs,
DisabledLibraryIDs: af.DisabledLibraryIDs,
MaxContentRating: af.MaxContentRating,
UserID: af.UserID,
ProfileID: af.ProfileID,
}
if v := q.Get("library_id"); v != "" {
id, err := strconv.Atoi(v)
if err != nil || id <= 0 {
writeError(w, http.StatusBadRequest, "bad_request", "library_id must be a positive integer")
return
}
cf.LibraryID = &id
}
events, err := h.repo.ListEvents(r.Context(), cf)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal_error", "failed to fetch calendar events")
return
}
// Group events by date and build response.
days := groupEventsByDate(events, r, h.detailSvc)
writeJSON(w, http.StatusOK, calendarResponse{Events: days})
}
func groupEventsByDate(events []catalog.CalendarEvent, r *http.Request, detailSvc *catalog.DetailService) []calendarDayResponse {
if len(events) == 0 {
return []calendarDayResponse{}
}
posterURLs := map[string]string{}
if detailSvc != nil {
posterPaths := make([]string, 0, len(events))
seenPosterPaths := make(map[string]struct{}, len(events))
for _, ev := range events {
if ev.PosterPath == "" {
continue
}
if _, ok := seenPosterPaths[ev.PosterPath]; ok {
continue
}
seenPosterPaths[ev.PosterPath] = struct{}{}
posterPaths = append(posterPaths, ev.PosterPath)
}
posterURLs = detailSvc.PresignImageURLs(r.Context(), posterPaths, "poster", "small")
}
var days []calendarDayResponse
var currentDay *calendarDayResponse
for _, ev := range events {
dateStr := ev.AirDate.Format("2006-01-02")
if currentDay == nil || currentDay.Date != dateStr {
if currentDay != nil {
days = append(days, *currentDay)
}
currentDay = &calendarDayResponse{Date: dateStr}
}
badges := buildBadges(ev)
currentDay.Items = append(currentDay.Items, calendarEventResponse{
ContentID: ev.ContentID,
Type: ev.Type,
Title: ev.Title,
EpisodeTitle: ev.EpisodeTitle,
SeriesID: ev.SeriesID,
SeasonNumber: ev.SeasonNumber,
EpisodeNumber: ev.EpisodeNumber,
AirDate: dateStr,
AirTime: ev.AirTime,
PosterURL: posterURLs[ev.PosterPath],
PosterThumbhash: ev.PosterThumbhash,
Badges: badges,
})
}
if currentDay != nil {
days = append(days, *currentDay)
}
return days
}
func buildBadges(ev catalog.CalendarEvent) []string {
var badges []string
if ev.IsPremiere {
if ev.SeasonNumber != nil && *ev.SeasonNumber == 1 {
badges = append(badges, "series_premiere")
} else {
badges = append(badges, "season_premiere")
}
}
if ev.IsFinale {
badges = append(badges, "finale")
}
if badges == nil {
badges = []string{}
}
return badges
}