package store import ( "context" "fmt" "strings" "time" ) // MediaRequest is one viewer's ask, as recorded. It carries no status: see the schema // comment on media_requests for why the state is derived per read rather than stored. type MediaRequest struct { MediaType string `json:"mediaType"` ForeignID int `json:"foreignId"` Title string `json:"title"` Year int `json:"year,omitempty"` PosterURL string `json:"posterUrl,omitempty"` RequestedAt time.Time `json:"requestedAt"` } // MediaRequestLimit caps what one viewer's page will read back. A household that has been // asking for things for two years should not turn the page into an unbounded query, and // nobody scrolls past the most recent hundred by remote. const MediaRequestLimit = 100 // SaveMediaRequest records an ask, or refreshes one already held. // // The repeat is deliberately an update rather than a no-op: asking again is how somebody // says they still want it, and the page is ordered by when they asked. func (s *Store) SaveMediaRequest(ctx context.Context, userID string, req MediaRequest) error { userID = strings.TrimSpace(userID) if userID == "" || req.ForeignID <= 0 { return fmt.Errorf("store: media request needs a user and a foreign id") } _, err := s.pool.Exec(ctx, ` INSERT INTO media_requests (emby_user_id, media_type, foreign_id, title, year, poster_url, requested_at) VALUES ($1, $2, $3, $4, $5, $6, now()) ON CONFLICT (emby_user_id, media_type, foreign_id) DO UPDATE SET title = EXCLUDED.title, year = EXCLUDED.year, poster_url = EXCLUDED.poster_url, requested_at = now()`, userID, req.MediaType, req.ForeignID, req.Title, req.Year, req.PosterURL) if err != nil { return fmt.Errorf("store: save media request: %w", err) } return nil } // MediaRequests returns one viewer's asks, most recent first. func (s *Store) MediaRequests(ctx context.Context, userID string) ([]MediaRequest, error) { rows, err := s.pool.Query(ctx, ` SELECT media_type, foreign_id, title, year, poster_url, requested_at FROM media_requests WHERE emby_user_id = $1 ORDER BY requested_at DESC LIMIT $2`, strings.TrimSpace(userID), MediaRequestLimit) if err != nil { return nil, fmt.Errorf("store: read media requests: %w", err) } defer rows.Close() requests := []MediaRequest{} for rows.Next() { var req MediaRequest if err := rows.Scan( &req.MediaType, &req.ForeignID, &req.Title, &req.Year, &req.PosterURL, &req.RequestedAt, ); err != nil { return nil, fmt.Errorf("store: scan media request: %w", err) } requests = append(requests, req) } return requests, rows.Err() } // DeleteMediaRequest removes a viewer's ask from their own page. // // It deliberately does not touch Radarr or Sonarr. The title may well have been downloaded // by now, and other people may have asked for it too — this only says the viewer no longer // wants it listed among theirs. func (s *Store) DeleteMediaRequest( ctx context.Context, userID, mediaType string, foreignID int, ) error { _, err := s.pool.Exec(ctx, ` DELETE FROM media_requests WHERE emby_user_id = $1 AND media_type = $2 AND foreign_id = $3`, strings.TrimSpace(userID), mediaType, foreignID) if err != nil { return fmt.Errorf("store: delete media request: %w", err) } return nil }