0.2.55 - Remote config/Request fixes

This commit is contained in:
ponzischeme89
2026-08-12 09:57:56 +12:00
parent 4b31946635
commit 9777eb0952
35 changed files with 1086 additions and 78 deletions
+169
View File
@@ -0,0 +1,169 @@
package config
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
)
// RemoteConfig is the deliberately small, presentation-only document offered to TVs.
// It must never contain authentication, playback or navigation-routing decisions: an
// unavailable document is required to be indistinguishable from an ordinary offline
// launch apart from its wording and safe presentation choices.
type RemoteConfig struct {
SchemaVersion int `json:"schemaVersion"`
ConfigVersion int64 `json:"configVersion"`
MinimumAppVersion string `json:"minimumAppVersion,omitempty"`
MaximumAppVersion string `json:"maximumAppVersion,omitempty"`
Copy RemoteConfigCopy `json:"copy"`
Features RemoteConfigFeatures `json:"features"`
Presentation RemoteConfigPresentation `json:"presentation"`
}
type RemoteConfigCopy struct {
Navigation RemoteConfigNavigationCopy `json:"navigation"`
Tagline string `json:"tagline"`
}
type RemoteConfigNavigationCopy struct {
Home string `json:"home"`
ForYou string `json:"forYou"`
Search string `json:"search"`
Movies string `json:"movies"`
TVShows string `json:"tvShows"`
TVCalendar string `json:"tvCalendar"`
Favourites string `json:"favourites"`
User string `json:"user"`
Settings string `json:"settings"`
}
type RemoteConfigFeatures struct {
ShowNavigationVersion bool `json:"showNavigationVersion"`
}
type RemoteConfigPresentation struct {
NavigationRailExpandedWidthDp int `json:"navigationRailExpandedWidthDp"`
NavigationContentShiftDp int `json:"navigationContentShiftDp"`
}
// DefaultRemoteConfig mirrors the APK's bundled values. Serving it is still useful: it
// establishes the schema and ETag contract before an operator chooses an override.
func DefaultRemoteConfig() RemoteConfig {
return RemoteConfig{
SchemaVersion: 1,
ConfigVersion: 1,
Copy: RemoteConfigCopy{
Tagline: "Matts Android TV client",
Navigation: RemoteConfigNavigationCopy{
Home: "Home", ForYou: "For You", Search: "Search", Movies: "Movies",
TVShows: "TV Shows", TVCalendar: "TV Calendar", Favourites: "Favourites",
User: "User", Settings: "Settings",
},
},
Features: RemoteConfigFeatures{ShowNavigationVersion: true},
Presentation: RemoteConfigPresentation{
NavigationRailExpandedWidthDp: 184,
NavigationContentShiftDp: 112,
},
}
}
func loadRemoteConfig(raw string) (RemoteConfig, error) {
if strings.TrimSpace(raw) == "" {
return DefaultRemoteConfig(), nil
}
var document RemoteConfig
decoder := json.NewDecoder(strings.NewReader(raw))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&document); err != nil {
return RemoteConfig{}, fmt.Errorf("MEMBY_REMOTE_CONFIG_JSON: %w", err)
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF {
return RemoteConfig{}, fmt.Errorf("MEMBY_REMOTE_CONFIG_JSON must contain exactly one document")
}
if err := validateRemoteConfig(document); err != nil {
return RemoteConfig{}, fmt.Errorf("MEMBY_REMOTE_CONFIG_JSON: %w", err)
}
return document, nil
}
func validateRemoteConfig(document RemoteConfig) error {
if document.SchemaVersion != 1 {
return fmt.Errorf("schemaVersion must be 1")
}
if document.ConfigVersion < 1 {
return fmt.Errorf("configVersion must be positive")
}
minimum, minimumSet, err := parseRemoteConfigVersion(document.MinimumAppVersion)
if err != nil {
return fmt.Errorf("minimumAppVersion must be a three-part version")
}
maximum, maximumSet, err := parseRemoteConfigVersion(document.MaximumAppVersion)
if err != nil {
return fmt.Errorf("maximumAppVersion must be a three-part version")
}
if minimumSet && maximumSet && compareRemoteConfigVersions(minimum, maximum) > 0 {
return fmt.Errorf("minimumAppVersion must not be newer than maximumAppVersion")
}
labels := []string{
document.Copy.Tagline,
document.Copy.Navigation.Home,
document.Copy.Navigation.ForYou,
document.Copy.Navigation.Search,
document.Copy.Navigation.Movies,
document.Copy.Navigation.TVShows,
document.Copy.Navigation.TVCalendar,
document.Copy.Navigation.Favourites,
document.Copy.Navigation.User,
document.Copy.Navigation.Settings,
}
for _, label := range labels {
trimmed := strings.TrimSpace(label)
if trimmed == "" || len([]rune(trimmed)) > 64 || strings.ContainsAny(trimmed, "\r\n\t") {
return fmt.Errorf("copy must be between 1 and 64 characters and contain no control whitespace")
}
}
width := document.Presentation.NavigationRailExpandedWidthDp
shift := document.Presentation.NavigationContentShiftDp
if width < 160 || width > 240 {
return fmt.Errorf("navigationRailExpandedWidthDp must be between 160 and 240")
}
if shift < 80 || shift > 160 || shift >= width {
return fmt.Errorf("navigationContentShiftDp must be between 80 and 160 and less than the rail width")
}
return nil
}
func parseRemoteConfigVersion(raw string) ([3]int, bool, error) {
var version [3]int
if strings.TrimSpace(raw) == "" {
return version, false, nil
}
parts := strings.Split(raw, ".")
if len(parts) != len(version) {
return version, false, fmt.Errorf("invalid version")
}
for index, part := range parts {
value, err := strconv.Atoi(part)
if err != nil || value < 0 {
return version, false, fmt.Errorf("invalid version")
}
version[index] = value
}
return version, true, nil
}
func compareRemoteConfigVersions(left, right [3]int) int {
for index := range left {
if left[index] < right[index] {
return -1
}
if left[index] > right[index] {
return 1
}
}
return 0
}