Files

84 lines
3.2 KiB
Go
Raw Permalink Normal View History

2026-08-09 08:25:50 +12:00
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
})
}