package store import ( "context" "fmt" "github.com/jackc/pgx/v5" ) // Which colour schemes an operator has decided a viewer may choose from. // // The store deliberately knows nothing about what a theme *is*: the catalogue, the palettes // and the rule about seasons all live in internal/api next to the client contract, the same // division user_preferences draws. What is here is only the set of ids, and the one property // that has to be true at this level — that an empty result means "unrestricted", never "no // themes at all". See the table comment in schema.sql. // UserThemes is the ids this viewer may pick between, or an empty slice for anybody the // operator has never restricted — which is everybody, until they do. func (s *Store) UserThemes(ctx context.Context, userID string) ([]string, error) { rows, err := s.pool.Query(ctx, `SELECT theme_id FROM user_themes WHERE emby_user_id = $1 ORDER BY theme_id`, userID) if err != nil { return nil, fmt.Errorf("store: read user themes: %w", err) } defer rows.Close() themes := []string{} for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, fmt.Errorf("store: scan user theme: %w", err) } themes = append(themes, id) } return themes, rows.Err() } // AllUserThemes is the admin console's read: one query for the whole accounts page rather // than one per person, since that page already fans out over every account it lists. func (s *Store) AllUserThemes(ctx context.Context) (map[string][]string, error) { rows, err := s.pool.Query(ctx, `SELECT emby_user_id, theme_id FROM user_themes ORDER BY emby_user_id, theme_id`) if err != nil { return nil, fmt.Errorf("store: list user themes: %w", err) } defer rows.Close() all := map[string][]string{} for rows.Next() { var userID, themeID string if err := rows.Scan(&userID, &themeID); err != nil { return nil, fmt.Errorf("store: scan user themes: %w", err) } all[userID] = append(all[userID], themeID) } return all, rows.Err() } // SetUserThemes replaces this viewer's allowlist wholesale. // // Replace rather than merge because the console sends the whole set of ticked boxes, and a // merge would make unticking one impossible. It is one transaction so a viewer is never // momentarily allowed nothing — a television resolving its theme in that window would be // told the default and would repaint itself for no reason. // // An empty list deletes the rows rather than storing anything, which is what keeps // "unrestricted" a single representation. api.normalizeThemeAllowlist is what turns "every // box ticked" into that empty list before it arrives here. func (s *Store) SetUserThemes(ctx context.Context, userID string, themes []string) error { return pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error { if _, err := tx.Exec(ctx, `DELETE FROM user_themes WHERE emby_user_id = $1`, userID); err != nil { return fmt.Errorf("store: clear user themes: %w", err) } for _, id := range themes { if _, err := tx.Exec(ctx, ` INSERT INTO user_themes (emby_user_id, theme_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, userID, id); err != nil { return fmt.Errorf("store: write user theme: %w", err) } } return nil }) }