0.3.22 - PINs
This commit is contained in:
@@ -80,6 +80,121 @@ var configurationCatalogue = []configurationDefinition{
|
||||
|
||||
func intPtr(v int) *int { return &v }
|
||||
|
||||
// rowTypeDefinition is the vocabulary behind the "row type" picker in the admin console's
|
||||
// visual row editor. An operator configuring Home, Movies or TV composition should never
|
||||
// need to know that a Continue Watching row is a "mediaRow" reading "emby.resume" — they
|
||||
// pick the type, and component/dataSource follow from it. It exists so the client's fixed,
|
||||
// small vocabulary of renderable components (mediaRow, mediaGrid, genreBrowser) is described
|
||||
// once, here, rather than the admin console guessing at it independently and drifting from
|
||||
// what a television actually understands.
|
||||
type rowTypeDefinition struct {
|
||||
Type string `json:"type"`
|
||||
Label string `json:"label"`
|
||||
Description string `json:"description"`
|
||||
Component string `json:"component"`
|
||||
DataSource string `json:"dataSource"`
|
||||
// Pages lists which of Home/Movies/TV this type may be placed on. A row whose type
|
||||
// is not valid for the page it is being saved to is exactly the "unsupported row/page
|
||||
// combination" the visual editor exists to make impossible.
|
||||
Pages []string `json:"pages"`
|
||||
// RequiresMatchingDestination is true for a type whose row only makes sense pointing at
|
||||
// the page it lives on (Genres/Library on Movies must browse Movies, never TV).
|
||||
RequiresMatchingDestination bool `json:"requiresMatchingDestination"`
|
||||
// Custom withholds component/dataSource entirely: the operator states them directly,
|
||||
// which is the escape hatch for a row this catalogue has no opinion about yet.
|
||||
Custom bool `json:"custom"`
|
||||
}
|
||||
|
||||
var rowTypeCatalogue = []rowTypeDefinition{
|
||||
{Type: "continueWatching", Label: "Continue Watching", Description: "Resumable films and unwatched next episodes, merged and ordered by recency.", Component: "mediaRow", DataSource: "emby.resume", Pages: []string{"home"}},
|
||||
{Type: "forYou", Label: "For You", Description: "Personalised recommendations built from this household's viewing history.", Component: "mediaRow", DataSource: "gateway.recommendations", Pages: []string{"home"}},
|
||||
{Type: "favorites", Label: "Favourites", Description: "Titles marked as favourites.", Component: "mediaRow", DataSource: "emby.favourites", Pages: []string{"home"}},
|
||||
{Type: "latest", Label: "Recently Added", Description: "The newest titles added to the library.", Component: "mediaRow", DataSource: "emby.latest", Pages: []string{"home"}},
|
||||
{Type: "genres", Label: "Genre Browser", Description: "A full-width row of genre categories for browsing by taste.", Component: "genreBrowser", DataSource: "emby.genres", Pages: []string{"movies", "tv"}, RequiresMatchingDestination: true},
|
||||
{Type: "library", Label: "Library Grid", Description: "A paged grid of the whole catalogue for this page.", Component: "mediaGrid", DataSource: "emby.library", Pages: []string{"movies", "tv"}, RequiresMatchingDestination: true},
|
||||
{Type: "custom", Label: "Custom", Description: "Set the data source and component directly. For development and configurations this catalogue does not yet describe.", Pages: []string{"home", "movies", "tv"}, Custom: true},
|
||||
}
|
||||
|
||||
func rowTypeDefinitionFor(rowType string) (rowTypeDefinition, bool) {
|
||||
for _, definition := range rowTypeCatalogue {
|
||||
if definition.Type == rowType {
|
||||
return definition, true
|
||||
}
|
||||
}
|
||||
return rowTypeDefinition{}, false
|
||||
}
|
||||
|
||||
// sectionDefinitionPageKeys maps a configuration key to the page it composes, which is what
|
||||
// lets validateSectionDefinitions refuse a row type placed on a page it does not support.
|
||||
var sectionDefinitionPageKeys = map[string]string{
|
||||
"home.sectionDefinitions": "home",
|
||||
"movies.sectionDefinitions": "movies",
|
||||
"tv.sectionDefinitions": "tv",
|
||||
}
|
||||
|
||||
// validateSectionDefinitions is the structural half of what the visual row editor promises:
|
||||
// invalid configurations are impossible to save, not merely discouraged. It runs beside the
|
||||
// generic "json" type check in validateConfigurationValue, which only confirms the value is
|
||||
// an array at all.
|
||||
func validateSectionDefinitions(key string, raw json.RawMessage) error {
|
||||
page, ok := sectionDefinitionPageKeys[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var definitions []config.RemoteSectionDefinition
|
||||
if err := json.Unmarshal(raw, &definitions); err != nil {
|
||||
return errors.New(key + ": invalid section definitions")
|
||||
}
|
||||
if len(definitions) > 32 {
|
||||
return errors.New(key + ": too many rows")
|
||||
}
|
||||
seenIDs := make(map[string]bool, len(definitions))
|
||||
seenPositions := make(map[int]bool, len(definitions))
|
||||
for _, definition := range definitions {
|
||||
id := strings.TrimSpace(definition.ID)
|
||||
if id == "" {
|
||||
return errors.New(key + ": every row needs an id")
|
||||
}
|
||||
if seenIDs[id] {
|
||||
return errors.New(key + ": duplicate row id " + id)
|
||||
}
|
||||
seenIDs[id] = true
|
||||
if strings.TrimSpace(definition.Title) == "" {
|
||||
return errors.New(key + ": row " + id + " needs a title")
|
||||
}
|
||||
if definition.Position < 0 {
|
||||
return errors.New(key + ": row " + id + " has an invalid position")
|
||||
}
|
||||
if seenPositions[definition.Position] {
|
||||
return errors.New(key + ": duplicate row position for " + id)
|
||||
}
|
||||
seenPositions[definition.Position] = true
|
||||
if definition.MaxItems < 0 || definition.MaxItems > 100 {
|
||||
return errors.New(key + ": row " + id + " has an invalid maximum item count")
|
||||
}
|
||||
if strings.TrimSpace(definition.Component) == "" {
|
||||
return errors.New(key + ": row " + id + " needs a component")
|
||||
}
|
||||
rowType, known := rowTypeDefinitionFor(definition.Type)
|
||||
if !known || rowType.Custom {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(rowType.Pages, page) {
|
||||
return errors.New(key + ": " + rowType.Label + " rows are not supported on this page")
|
||||
}
|
||||
if definition.Component != rowType.Component {
|
||||
return errors.New(key + ": row " + id + " has a component that does not match its row type")
|
||||
}
|
||||
if definition.DataSource != rowType.DataSource {
|
||||
return errors.New(key + ": row " + id + " has a data source that does not match its row type")
|
||||
}
|
||||
if rowType.RequiresMatchingDestination && definition.Destination != page {
|
||||
return errors.New(key + ": row " + id + " must target the " + page + " page")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var featureCatalogue = []featureDefinition{
|
||||
{
|
||||
Key: featureSonarrPreroll, Name: "Sonarr upcoming preroll", Area: "Playback",
|
||||
@@ -236,6 +351,10 @@ type featureResponse struct {
|
||||
CanRollback bool `json:"canRollback"`
|
||||
Features []evaluatedFeature `json:"features"`
|
||||
Configuration []evaluatedConfiguration `json:"configuration"`
|
||||
// RowTypes is the static catalogue behind the console's visual row editor. It travels
|
||||
// with every feature-policy response rather than a dedicated endpoint, because it is
|
||||
// read alongside Configuration and never changes independently of a server release.
|
||||
RowTypes []rowTypeDefinition `json:"rowTypes"`
|
||||
}
|
||||
|
||||
type evaluatedConfiguration struct {
|
||||
@@ -390,6 +509,7 @@ func featurePayloadForSession(policy store.FeaturePolicy, protocol int, session
|
||||
SchemaVersion: featureSchemaVersion, Revision: policy.Revision,
|
||||
SafeMode: policy.SafeMode, UpdatedAt: policy.UpdatedAt,
|
||||
CanRollback: policy.Previous != nil, Features: features,
|
||||
RowTypes: rowTypeCatalogue,
|
||||
}
|
||||
if session != nil {
|
||||
response.Configuration = configurationPayload(policy, *session)
|
||||
@@ -555,6 +675,9 @@ func validateConfigurationValue(definition configurationDefinition, raw json.Raw
|
||||
if _, ok := value.([]any); !ok {
|
||||
return errors.New("configuration value must be a JSON array: " + definition.Key)
|
||||
}
|
||||
if err := validateSectionDefinitions(definition.Key, raw); err != nil {
|
||||
return err
|
||||
}
|
||||
case "string":
|
||||
if _, ok := value.(string); !ok {
|
||||
return errors.New("configuration value must be text: " + definition.Key)
|
||||
|
||||
Reference in New Issue
Block a user