0.3.22 - PINs

This commit is contained in:
ponzischeme89
2026-08-25 11:39:55 +12:00
parent 396d35e2f5
commit 0fcc02f57e
2697 changed files with 5360 additions and 50 deletions
+123
View File
@@ -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)