Skip to content

Changelog


2026-07-01

Initial implementation — ScheduleSlot CRUD + EPG queries

Why: PlayTelly channels needed a program timetable layer so the OTT client can display a guide overlay alongside the live stream. EPG data is orthogonal to stream configuration — a channel plays regardless of whether EPG exists, and EPG is purely informational metadata on top of whatever is airing.

What was built:

models.goScheduleSlot struct, table schedule_slots. Loose FK to channels.id (integer only, no struct import). All times stored as UTC — frontend converts to viewer local timezone.

handler_schedule.go — full CRUD with overlap validation. Two slots cannot overlap on the same channel — 409 Conflict returned if they would. On update, the current slot is excluded from the overlap check so title/genre updates never trigger a false positive.

handler_epg.go — three EPG query endpoints: - GET /epg?date= — EPG grid, all channels for a day. Single call drives the full grid UI. Joins channels table via raw SQL (no playback model import). Returns empty channels array with hint if no channels exist. - GET /epg/:channelId?date= — single channel, one day. - GET /epg/:channelId/now — currently airing program + next program. now is null if nothing is scheduled for the current time.

Design decisions:

  • Scheduling domain does not import domain/playback — channel identity is accessed via raw SQL join on id, sid, name only. Zero circular imports.
  • channelRow local struct in GetEPGGrid uses explicit gorm:"column:..." tags (gorm:"column:sid") to avoid GORM's all-caps naming convention converting SIDs_id in raw scan. Pattern mirrors how Channel.SID is declared in the playback domain model.
  • EPGEntry, BlackoutRule, RecurrenceRule are spec'd entities deferred to future iterations — stubs exist in handler_blackout.go and handler_recurrence.go.
  • SCHEDULING_NATS_ENABLED env var is wired in config.go but not yet acted on — placeholder for the "emits facts onto the bus" behaviour the domain spec requires.

Bug fixed during initial build:

UpdateSlot had a duplicate time-parsing block — start/end times were parsed twice, first without setting newStart/newEnd, then again correctly. The first redundant block was removed, keeping only the block that sets both updates map and the newStart/newEnd variables used by the overlap check.