From 13d7cccbff35c287efd146fba7942fc75e38e680 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:37:29 -0400 Subject: [PATCH 01/17] feat(watchsync): expand provider plugin contract (#13) * feat(watchsync): expand provider plugin contract * fix(watchsync): preserve provider compatibility * docs(watchsync): define list tombstones * docs(watchsync): require snapshots for ordered lists * fix(watchsync): preserve device auth compatibility --- README.md | 52 +- .../silo/plugin/v1/watch_sync_provider.pb.go | 1082 ++++++++++++++--- .../v1/watch_sync_provider_compat_test.go | 39 + .../plugin/v1/watch_sync_provider_grpc.pb.go | 146 +++ .../plugin/v1/watch_sync_provider_test.go | 127 +- .../convert/watch_sync_provider_test.go | 11 +- pkg/pluginsdk/manifest/manifest.go | 10 +- .../manifest/watch_sync_provider_test.go | 22 + .../runtime/capability_servers_compat_test.go | 18 + pkg/pluginsdk/runtime/runtime.go | 38 + pkg/pluginsdk/runtime/serve_manifest.go | 43 +- .../runtime/watch_sync_provider_test.go | 20 +- .../silo/plugin/v1/watch_sync_provider.proto | 117 +- 13 files changed, 1520 insertions(+), 205 deletions(-) create mode 100644 pkg/pluginproto/silo/plugin/v1/watch_sync_provider_compat_test.go create mode 100644 pkg/pluginsdk/runtime/capability_servers_compat_test.go diff --git a/README.md b/README.md index b355010..71d8db5 100644 --- a/README.md +++ b/README.md @@ -126,15 +126,16 @@ The `auth_provider.v1` capability also exposes OAuth-flow RPCs (`InitAuthorize`, `watch_sync_provider.v1` lets external plugins participate in Silo's host-owned watch-provider pipeline. The host owns encrypted per-profile credentials, -OAuth state, durable desired-state events, retries, ordering, and -reconciliation. Plugins are stateless protocol adapters: they receive secrets -only for the duration of an RPC, map rich movie/episode identity to an upstream -service, and return typed apply or retry outcomes. +authorization-code and device-code flow state, durable desired-state events, +retries, ordering, and reconciliation. Plugins are stateless protocol adapters: +they receive secrets only for the duration of an RPC, map rich movie/episode +identity to an upstream service, and return typed apply or retry outcomes. Watch-sync plugins must not persist or log credentials, authorization codes, provider flow state, or secret configuration. `ApplyEvents` is an at-least-once contract; plugins must treat `event_id` as stable across retries and implement -convergent desired-state updates rather than increments. +convergent desired-state updates rather than increments. That rule also applies +to scrobble stops: replaying the same event ID must not create another play. Authenticated RPCs receive the same host-owned capability, configuration, and credential data through `WatchSyncAuthenticatedContext`. The context exists @@ -144,6 +145,29 @@ patches. The host validates and persists them before consuming results, pages, or faults—even when the response contains a fault. If credential persistence fails, the host commits no other response data. +Device-code plugins register both `WatchSyncProvider` and the separate +`WatchSyncDeviceAuthorizationService`. Keeping device authorization in a +second service preserves source compatibility for v0.12 Go providers that +implemented `WatchSyncProviderServer` directly. Register it without changing +the released `CapabilityServers` shape: + +```go +runtime.ServeManifestWithOptions(manifestJSON, version, servers, + runtime.WithWatchSyncDeviceAuthorization(deviceAuthServer)) +``` + +A pending poll may replace its opaque provider state, polling interval, and +expiry; the host encrypts and persists those values before the next poll. +Those updates remain part of the same user challenge, so the original user code +and verification URL must stay valid until expiry. An explicitly empty +`provider_state` clears the prior state; omitting it retains the prior state. + +`WatchSyncProviderConfig` is keyed by manifest config key and field, for example +`provider.client_id`. Scalar values are sent as strings and structured values +as JSON. Fields marked secret in the manifest are sent through `secret_values`; +undeclared fields are treated as secret. Plugins must accept configuration from +the RPC context rather than relying on process-global state. + Descriptors and events use the shared `WatchSyncMediaType` enum so advertised support and delivered media cannot drift between string conventions. Apply results pair their delivery status with a typed fault: successful results omit @@ -153,11 +177,19 @@ Connection-wide faults such as invalid credentials belong on the RPC response. `ListRemoteState` returns provider-neutral typed subrecords. `watched` carries a play count and last-watched time; `progress` carries a fractional percentage and -paused time. An item may contain either or both. The host keeps the request -`cursor` fixed while following ephemeral page tokens, commits each successful -page, and only then persists the final `next_cursor`. `complete_snapshot=true` -means the traversal is authoritative; when false, missing items are not -deletions. +paused time; `favorite` and `watchlist` carry list membership. An item may +contain multiple state families. The host requests only the state families a +sync phase needs, keeps that phase's `cursor` fixed while following ephemeral +page tokens, commits each successful page, and only then persists the final +`next_cursor`. `complete_snapshot=true` means the traversal is authoritative; +when false, missing items are not deletions. An incremental favorite or +watchlist removal is an item whose corresponding list state has `removed=true`; +it may omit `media` when `provider_item_key` identifies a record previously +returned to the host. When +`provides_watchlist_order=true`, watchlist traversals must be complete snapshots +and the order of returned watchlist states is the remote list order. Event +`list_position` is presence-aware: an explicit zero means the first position, +while omission means no requested ordering. ## Scan sources diff --git a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider.pb.go b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider.pb.go index 7b21e9c..91bfbd2 100644 --- a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider.pb.go @@ -30,6 +30,7 @@ const ( WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_UNSPECIFIED WatchSyncAuthMethod = 0 WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_AUTHORIZATION_CODE WatchSyncAuthMethod = 1 WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_API_KEY WatchSyncAuthMethod = 2 + WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_DEVICE_CODE WatchSyncAuthMethod = 3 ) // Enum value maps for WatchSyncAuthMethod. @@ -38,11 +39,13 @@ var ( 0: "WATCH_SYNC_AUTH_METHOD_UNSPECIFIED", 1: "WATCH_SYNC_AUTH_METHOD_AUTHORIZATION_CODE", 2: "WATCH_SYNC_AUTH_METHOD_API_KEY", + 3: "WATCH_SYNC_AUTH_METHOD_DEVICE_CODE", } WatchSyncAuthMethod_value = map[string]int32{ "WATCH_SYNC_AUTH_METHOD_UNSPECIFIED": 0, "WATCH_SYNC_AUTH_METHOD_AUTHORIZATION_CODE": 1, "WATCH_SYNC_AUTH_METHOD_API_KEY": 2, + "WATCH_SYNC_AUTH_METHOD_DEVICE_CODE": 3, } ) @@ -125,9 +128,16 @@ func (WatchSyncMediaType) EnumDescriptor() ([]byte, []int) { type WatchSyncOperation int32 const ( - WatchSyncOperation_WATCH_SYNC_OPERATION_UNSPECIFIED WatchSyncOperation = 0 - WatchSyncOperation_WATCH_SYNC_OPERATION_MARK_WATCHED WatchSyncOperation = 1 - WatchSyncOperation_WATCH_SYNC_OPERATION_MARK_UNWATCHED WatchSyncOperation = 2 + WatchSyncOperation_WATCH_SYNC_OPERATION_UNSPECIFIED WatchSyncOperation = 0 + WatchSyncOperation_WATCH_SYNC_OPERATION_MARK_WATCHED WatchSyncOperation = 1 + WatchSyncOperation_WATCH_SYNC_OPERATION_MARK_UNWATCHED WatchSyncOperation = 2 + WatchSyncOperation_WATCH_SYNC_OPERATION_ADD_FAVORITE WatchSyncOperation = 3 + WatchSyncOperation_WATCH_SYNC_OPERATION_REMOVE_FAVORITE WatchSyncOperation = 4 + WatchSyncOperation_WATCH_SYNC_OPERATION_ADD_TO_WATCHLIST WatchSyncOperation = 5 + WatchSyncOperation_WATCH_SYNC_OPERATION_REMOVE_FROM_WATCHLIST WatchSyncOperation = 6 + WatchSyncOperation_WATCH_SYNC_OPERATION_SCROBBLE_START WatchSyncOperation = 7 + WatchSyncOperation_WATCH_SYNC_OPERATION_SCROBBLE_PAUSE WatchSyncOperation = 8 + WatchSyncOperation_WATCH_SYNC_OPERATION_SCROBBLE_STOP WatchSyncOperation = 9 ) // Enum value maps for WatchSyncOperation. @@ -136,11 +146,25 @@ var ( 0: "WATCH_SYNC_OPERATION_UNSPECIFIED", 1: "WATCH_SYNC_OPERATION_MARK_WATCHED", 2: "WATCH_SYNC_OPERATION_MARK_UNWATCHED", + 3: "WATCH_SYNC_OPERATION_ADD_FAVORITE", + 4: "WATCH_SYNC_OPERATION_REMOVE_FAVORITE", + 5: "WATCH_SYNC_OPERATION_ADD_TO_WATCHLIST", + 6: "WATCH_SYNC_OPERATION_REMOVE_FROM_WATCHLIST", + 7: "WATCH_SYNC_OPERATION_SCROBBLE_START", + 8: "WATCH_SYNC_OPERATION_SCROBBLE_PAUSE", + 9: "WATCH_SYNC_OPERATION_SCROBBLE_STOP", } WatchSyncOperation_value = map[string]int32{ - "WATCH_SYNC_OPERATION_UNSPECIFIED": 0, - "WATCH_SYNC_OPERATION_MARK_WATCHED": 1, - "WATCH_SYNC_OPERATION_MARK_UNWATCHED": 2, + "WATCH_SYNC_OPERATION_UNSPECIFIED": 0, + "WATCH_SYNC_OPERATION_MARK_WATCHED": 1, + "WATCH_SYNC_OPERATION_MARK_UNWATCHED": 2, + "WATCH_SYNC_OPERATION_ADD_FAVORITE": 3, + "WATCH_SYNC_OPERATION_REMOVE_FAVORITE": 4, + "WATCH_SYNC_OPERATION_ADD_TO_WATCHLIST": 5, + "WATCH_SYNC_OPERATION_REMOVE_FROM_WATCHLIST": 6, + "WATCH_SYNC_OPERATION_SCROBBLE_START": 7, + "WATCH_SYNC_OPERATION_SCROBBLE_PAUSE": 8, + "WATCH_SYNC_OPERATION_SCROBBLE_STOP": 9, } ) @@ -171,6 +195,116 @@ func (WatchSyncOperation) EnumDescriptor() ([]byte, []int) { return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{2} } +type WatchSyncRemoteStateKind int32 + +const ( + WatchSyncRemoteStateKind_WATCH_SYNC_REMOTE_STATE_KIND_UNSPECIFIED WatchSyncRemoteStateKind = 0 + WatchSyncRemoteStateKind_WATCH_SYNC_REMOTE_STATE_KIND_WATCHED WatchSyncRemoteStateKind = 1 + WatchSyncRemoteStateKind_WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS WatchSyncRemoteStateKind = 2 + WatchSyncRemoteStateKind_WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE WatchSyncRemoteStateKind = 3 + WatchSyncRemoteStateKind_WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST WatchSyncRemoteStateKind = 4 +) + +// Enum value maps for WatchSyncRemoteStateKind. +var ( + WatchSyncRemoteStateKind_name = map[int32]string{ + 0: "WATCH_SYNC_REMOTE_STATE_KIND_UNSPECIFIED", + 1: "WATCH_SYNC_REMOTE_STATE_KIND_WATCHED", + 2: "WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS", + 3: "WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE", + 4: "WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST", + } + WatchSyncRemoteStateKind_value = map[string]int32{ + "WATCH_SYNC_REMOTE_STATE_KIND_UNSPECIFIED": 0, + "WATCH_SYNC_REMOTE_STATE_KIND_WATCHED": 1, + "WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS": 2, + "WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE": 3, + "WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST": 4, + } +) + +func (x WatchSyncRemoteStateKind) Enum() *WatchSyncRemoteStateKind { + p := new(WatchSyncRemoteStateKind) + *p = x + return p +} + +func (x WatchSyncRemoteStateKind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (WatchSyncRemoteStateKind) Descriptor() protoreflect.EnumDescriptor { + return file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[3].Descriptor() +} + +func (WatchSyncRemoteStateKind) Type() protoreflect.EnumType { + return &file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[3] +} + +func (x WatchSyncRemoteStateKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use WatchSyncRemoteStateKind.Descriptor instead. +func (WatchSyncRemoteStateKind) EnumDescriptor() ([]byte, []int) { + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{3} +} + +type WatchSyncDeviceAuthorizationStatus int32 + +const ( + WatchSyncDeviceAuthorizationStatus_WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_UNSPECIFIED WatchSyncDeviceAuthorizationStatus = 0 + WatchSyncDeviceAuthorizationStatus_WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_PENDING WatchSyncDeviceAuthorizationStatus = 1 + WatchSyncDeviceAuthorizationStatus_WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_AUTHORIZED WatchSyncDeviceAuthorizationStatus = 2 + WatchSyncDeviceAuthorizationStatus_WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_DENIED WatchSyncDeviceAuthorizationStatus = 3 + WatchSyncDeviceAuthorizationStatus_WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_EXPIRED WatchSyncDeviceAuthorizationStatus = 4 +) + +// Enum value maps for WatchSyncDeviceAuthorizationStatus. +var ( + WatchSyncDeviceAuthorizationStatus_name = map[int32]string{ + 0: "WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_UNSPECIFIED", + 1: "WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_PENDING", + 2: "WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_AUTHORIZED", + 3: "WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_DENIED", + 4: "WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_EXPIRED", + } + WatchSyncDeviceAuthorizationStatus_value = map[string]int32{ + "WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_UNSPECIFIED": 0, + "WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_PENDING": 1, + "WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_AUTHORIZED": 2, + "WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_DENIED": 3, + "WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_EXPIRED": 4, + } +) + +func (x WatchSyncDeviceAuthorizationStatus) Enum() *WatchSyncDeviceAuthorizationStatus { + p := new(WatchSyncDeviceAuthorizationStatus) + *p = x + return p +} + +func (x WatchSyncDeviceAuthorizationStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (WatchSyncDeviceAuthorizationStatus) Descriptor() protoreflect.EnumDescriptor { + return file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[4].Descriptor() +} + +func (WatchSyncDeviceAuthorizationStatus) Type() protoreflect.EnumType { + return &file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[4] +} + +func (x WatchSyncDeviceAuthorizationStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use WatchSyncDeviceAuthorizationStatus.Descriptor instead. +func (WatchSyncDeviceAuthorizationStatus) EnumDescriptor() ([]byte, []int) { + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{4} +} + type WatchSyncOrigin int32 const ( @@ -207,11 +341,11 @@ func (x WatchSyncOrigin) String() string { } func (WatchSyncOrigin) Descriptor() protoreflect.EnumDescriptor { - return file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[3].Descriptor() + return file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[5].Descriptor() } func (WatchSyncOrigin) Type() protoreflect.EnumType { - return &file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[3] + return &file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[5] } func (x WatchSyncOrigin) Number() protoreflect.EnumNumber { @@ -220,7 +354,7 @@ func (x WatchSyncOrigin) Number() protoreflect.EnumNumber { // Deprecated: Use WatchSyncOrigin.Descriptor instead. func (WatchSyncOrigin) EnumDescriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{3} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{5} } type WatchSyncApplyStatus int32 @@ -262,11 +396,11 @@ func (x WatchSyncApplyStatus) String() string { } func (WatchSyncApplyStatus) Descriptor() protoreflect.EnumDescriptor { - return file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[4].Descriptor() + return file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[6].Descriptor() } func (WatchSyncApplyStatus) Type() protoreflect.EnumType { - return &file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[4] + return &file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[6] } func (x WatchSyncApplyStatus) Number() protoreflect.EnumNumber { @@ -275,7 +409,7 @@ func (x WatchSyncApplyStatus) Number() protoreflect.EnumNumber { // Deprecated: Use WatchSyncApplyStatus.Descriptor instead. func (WatchSyncApplyStatus) EnumDescriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{4} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{6} } type WatchSyncFaultCode int32 @@ -323,11 +457,11 @@ func (x WatchSyncFaultCode) String() string { } func (WatchSyncFaultCode) Descriptor() protoreflect.EnumDescriptor { - return file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[5].Descriptor() + return file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[7].Descriptor() } func (WatchSyncFaultCode) Type() protoreflect.EnumType { - return &file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[5] + return &file_silo_plugin_v1_watch_sync_provider_proto_enumTypes[7] } func (x WatchSyncFaultCode) Number() protoreflect.EnumNumber { @@ -336,7 +470,7 @@ func (x WatchSyncFaultCode) Number() protoreflect.EnumNumber { // Deprecated: Use WatchSyncFaultCode.Descriptor instead. func (WatchSyncFaultCode) EnumDescriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{5} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{7} } type WatchSyncProviderDescriptor struct { @@ -349,8 +483,19 @@ type WatchSyncProviderDescriptor struct { SupportedMediaTypes []WatchSyncMediaType `protobuf:"varint,6,rep,packed,name=supported_media_types,json=supportedMediaTypes,proto3,enum=silo.plugin.v1.WatchSyncMediaType" json:"supported_media_types,omitempty"` ExternalIdNamespaces []string `protobuf:"bytes,7,rep,name=external_id_namespaces,json=externalIdNamespaces,proto3" json:"external_id_namespaces,omitempty"` MaxBatchSize int32 `protobuf:"varint,8,opt,name=max_batch_size,json=maxBatchSize,proto3" json:"max_batch_size,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + ImportFavorites bool `protobuf:"varint,9,opt,name=import_favorites,json=importFavorites,proto3" json:"import_favorites,omitempty"` + ExportFavorites bool `protobuf:"varint,10,opt,name=export_favorites,json=exportFavorites,proto3" json:"export_favorites,omitempty"` + RemoveFavorites bool `protobuf:"varint,11,opt,name=remove_favorites,json=removeFavorites,proto3" json:"remove_favorites,omitempty"` + ImportWatchlist bool `protobuf:"varint,12,opt,name=import_watchlist,json=importWatchlist,proto3" json:"import_watchlist,omitempty"` + ExportWatchlist bool `protobuf:"varint,13,opt,name=export_watchlist,json=exportWatchlist,proto3" json:"export_watchlist,omitempty"` + RemoveWatchlist bool `protobuf:"varint,14,opt,name=remove_watchlist,json=removeWatchlist,proto3" json:"remove_watchlist,omitempty"` + // The order of WATCHLIST states returned by ListRemoteState is meaningful. + // WATCHLIST traversals must set complete_snapshot=true when this is enabled; + // an incremental subset cannot define positions relative to omitted items. + ProvidesWatchlistOrder bool `protobuf:"varint,15,opt,name=provides_watchlist_order,json=providesWatchlistOrder,proto3" json:"provides_watchlist_order,omitempty"` + ScrobblePlayback bool `protobuf:"varint,16,opt,name=scrobble_playback,json=scrobblePlayback,proto3" json:"scrobble_playback,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *WatchSyncProviderDescriptor) Reset() { @@ -439,6 +584,62 @@ func (x *WatchSyncProviderDescriptor) GetMaxBatchSize() int32 { return 0 } +func (x *WatchSyncProviderDescriptor) GetImportFavorites() bool { + if x != nil { + return x.ImportFavorites + } + return false +} + +func (x *WatchSyncProviderDescriptor) GetExportFavorites() bool { + if x != nil { + return x.ExportFavorites + } + return false +} + +func (x *WatchSyncProviderDescriptor) GetRemoveFavorites() bool { + if x != nil { + return x.RemoveFavorites + } + return false +} + +func (x *WatchSyncProviderDescriptor) GetImportWatchlist() bool { + if x != nil { + return x.ImportWatchlist + } + return false +} + +func (x *WatchSyncProviderDescriptor) GetExportWatchlist() bool { + if x != nil { + return x.ExportWatchlist + } + return false +} + +func (x *WatchSyncProviderDescriptor) GetRemoveWatchlist() bool { + if x != nil { + return x.RemoveWatchlist + } + return false +} + +func (x *WatchSyncProviderDescriptor) GetProvidesWatchlistOrder() bool { + if x != nil { + return x.ProvidesWatchlistOrder + } + return false +} + +func (x *WatchSyncProviderDescriptor) GetScrobblePlayback() bool { + if x != nil { + return x.ScrobblePlayback + } + return false +} + type WatchSyncProviderConfig struct { state protoimpl.MessageState `protogen:"open.v1"` Values map[string]string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` @@ -1055,6 +1256,319 @@ func (x *WatchSyncExchangeAPIKeyRequest) GetApiKey() string { return "" } +type WatchSyncDeviceAuthorizationServiceStartRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + CapabilityId string `protobuf:"bytes,1,opt,name=capability_id,json=capabilityId,proto3" json:"capability_id,omitempty"` + ProviderConfig *WatchSyncProviderConfig `protobuf:"bytes,2,opt,name=provider_config,json=providerConfig,proto3" json:"provider_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WatchSyncDeviceAuthorizationServiceStartRequest) Reset() { + *x = WatchSyncDeviceAuthorizationServiceStartRequest{} + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WatchSyncDeviceAuthorizationServiceStartRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchSyncDeviceAuthorizationServiceStartRequest) ProtoMessage() {} + +func (x *WatchSyncDeviceAuthorizationServiceStartRequest) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchSyncDeviceAuthorizationServiceStartRequest.ProtoReflect.Descriptor instead. +func (*WatchSyncDeviceAuthorizationServiceStartRequest) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{10} +} + +func (x *WatchSyncDeviceAuthorizationServiceStartRequest) GetCapabilityId() string { + if x != nil { + return x.CapabilityId + } + return "" +} + +func (x *WatchSyncDeviceAuthorizationServiceStartRequest) GetProviderConfig() *WatchSyncProviderConfig { + if x != nil { + return x.ProviderConfig + } + return nil +} + +type WatchSyncDeviceAuthorizationServiceStartResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserCode string `protobuf:"bytes,1,opt,name=user_code,json=userCode,proto3" json:"user_code,omitempty"` + VerificationUrl string `protobuf:"bytes,2,opt,name=verification_url,json=verificationUrl,proto3" json:"verification_url,omitempty"` + VerificationUrlComplete string `protobuf:"bytes,3,opt,name=verification_url_complete,json=verificationUrlComplete,proto3" json:"verification_url_complete,omitempty"` + // Plugin-defined flow data. The host encrypts it until authorization + // completes and never returns it through the public HTTP API. + ProviderState []byte `protobuf:"bytes,4,opt,name=provider_state,json=providerState,proto3" json:"provider_state,omitempty"` + PollingInterval *durationpb.Duration `protobuf:"bytes,5,opt,name=polling_interval,json=pollingInterval,proto3" json:"polling_interval,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + Fault *WatchSyncFault `protobuf:"bytes,7,opt,name=fault,proto3" json:"fault,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WatchSyncDeviceAuthorizationServiceStartResponse) Reset() { + *x = WatchSyncDeviceAuthorizationServiceStartResponse{} + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WatchSyncDeviceAuthorizationServiceStartResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchSyncDeviceAuthorizationServiceStartResponse) ProtoMessage() {} + +func (x *WatchSyncDeviceAuthorizationServiceStartResponse) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchSyncDeviceAuthorizationServiceStartResponse.ProtoReflect.Descriptor instead. +func (*WatchSyncDeviceAuthorizationServiceStartResponse) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{11} +} + +func (x *WatchSyncDeviceAuthorizationServiceStartResponse) GetUserCode() string { + if x != nil { + return x.UserCode + } + return "" +} + +func (x *WatchSyncDeviceAuthorizationServiceStartResponse) GetVerificationUrl() string { + if x != nil { + return x.VerificationUrl + } + return "" +} + +func (x *WatchSyncDeviceAuthorizationServiceStartResponse) GetVerificationUrlComplete() string { + if x != nil { + return x.VerificationUrlComplete + } + return "" +} + +func (x *WatchSyncDeviceAuthorizationServiceStartResponse) GetProviderState() []byte { + if x != nil { + return x.ProviderState + } + return nil +} + +func (x *WatchSyncDeviceAuthorizationServiceStartResponse) GetPollingInterval() *durationpb.Duration { + if x != nil { + return x.PollingInterval + } + return nil +} + +func (x *WatchSyncDeviceAuthorizationServiceStartResponse) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *WatchSyncDeviceAuthorizationServiceStartResponse) GetFault() *WatchSyncFault { + if x != nil { + return x.Fault + } + return nil +} + +type WatchSyncDeviceAuthorizationServicePollRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + CapabilityId string `protobuf:"bytes,1,opt,name=capability_id,json=capabilityId,proto3" json:"capability_id,omitempty"` + ProviderConfig *WatchSyncProviderConfig `protobuf:"bytes,2,opt,name=provider_config,json=providerConfig,proto3" json:"provider_config,omitempty"` + ProviderState []byte `protobuf:"bytes,3,opt,name=provider_state,json=providerState,proto3" json:"provider_state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WatchSyncDeviceAuthorizationServicePollRequest) Reset() { + *x = WatchSyncDeviceAuthorizationServicePollRequest{} + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WatchSyncDeviceAuthorizationServicePollRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchSyncDeviceAuthorizationServicePollRequest) ProtoMessage() {} + +func (x *WatchSyncDeviceAuthorizationServicePollRequest) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchSyncDeviceAuthorizationServicePollRequest.ProtoReflect.Descriptor instead. +func (*WatchSyncDeviceAuthorizationServicePollRequest) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{12} +} + +func (x *WatchSyncDeviceAuthorizationServicePollRequest) GetCapabilityId() string { + if x != nil { + return x.CapabilityId + } + return "" +} + +func (x *WatchSyncDeviceAuthorizationServicePollRequest) GetProviderConfig() *WatchSyncProviderConfig { + if x != nil { + return x.ProviderConfig + } + return nil +} + +func (x *WatchSyncDeviceAuthorizationServicePollRequest) GetProviderState() []byte { + if x != nil { + return x.ProviderState + } + return nil +} + +type WatchSyncDeviceAuthorizationServicePollResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status WatchSyncDeviceAuthorizationStatus `protobuf:"varint,1,opt,name=status,proto3,enum=silo.plugin.v1.WatchSyncDeviceAuthorizationStatus" json:"status,omitempty"` + // Complete authoritative replacement when status is AUTHORIZED. The host + // persists it before consuming account or fault. + Credentials *WatchSyncCredentials `protobuf:"bytes,2,opt,name=credentials,proto3" json:"credentials,omitempty"` + Account *WatchSyncAccount `protobuf:"bytes,3,opt,name=account,proto3" json:"account,omitempty"` + // PENDING normally omits fault. DENIED and EXPIRED may use a safe, + // non-retryable fault. Connection-wide and transport failures use fault. + Fault *WatchSyncFault `protobuf:"bytes,4,opt,name=fault,proto3" json:"fault,omitempty"` + // A PENDING response may update host-private state and timing only for the + // same user challenge. The user_code and verification URLs returned by Start + // must remain valid through expires_at; if those instructions change, expire + // this flow and require a new Start call. + // + // Optional authoritative replacement for provider_state while PENDING. The + // host encrypts and uses it for the next poll. Presence is significant: an + // explicitly empty value clears the prior state, while omission retains it. + ProviderState []byte `protobuf:"bytes,5,opt,name=provider_state,json=providerState,proto3,oneof" json:"provider_state,omitempty"` + // Optional replacement polling interval while PENDING, for example after an + // OAuth device-flow slow_down response. + PollingInterval *durationpb.Duration `protobuf:"bytes,6,opt,name=polling_interval,json=pollingInterval,proto3" json:"polling_interval,omitempty"` + // Optional replacement expiry for the same user challenge. + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WatchSyncDeviceAuthorizationServicePollResponse) Reset() { + *x = WatchSyncDeviceAuthorizationServicePollResponse{} + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WatchSyncDeviceAuthorizationServicePollResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchSyncDeviceAuthorizationServicePollResponse) ProtoMessage() {} + +func (x *WatchSyncDeviceAuthorizationServicePollResponse) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchSyncDeviceAuthorizationServicePollResponse.ProtoReflect.Descriptor instead. +func (*WatchSyncDeviceAuthorizationServicePollResponse) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{13} +} + +func (x *WatchSyncDeviceAuthorizationServicePollResponse) GetStatus() WatchSyncDeviceAuthorizationStatus { + if x != nil { + return x.Status + } + return WatchSyncDeviceAuthorizationStatus_WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_UNSPECIFIED +} + +func (x *WatchSyncDeviceAuthorizationServicePollResponse) GetCredentials() *WatchSyncCredentials { + if x != nil { + return x.Credentials + } + return nil +} + +func (x *WatchSyncDeviceAuthorizationServicePollResponse) GetAccount() *WatchSyncAccount { + if x != nil { + return x.Account + } + return nil +} + +func (x *WatchSyncDeviceAuthorizationServicePollResponse) GetFault() *WatchSyncFault { + if x != nil { + return x.Fault + } + return nil +} + +func (x *WatchSyncDeviceAuthorizationServicePollResponse) GetProviderState() []byte { + if x != nil { + return x.ProviderState + } + return nil +} + +func (x *WatchSyncDeviceAuthorizationServicePollResponse) GetPollingInterval() *durationpb.Duration { + if x != nil { + return x.PollingInterval + } + return nil +} + +func (x *WatchSyncDeviceAuthorizationServicePollResponse) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + type WatchSyncRefreshCredentialsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Context *WatchSyncAuthenticatedContext `protobuf:"bytes,1,opt,name=context,proto3" json:"context,omitempty"` @@ -1064,7 +1578,7 @@ type WatchSyncRefreshCredentialsRequest struct { func (x *WatchSyncRefreshCredentialsRequest) Reset() { *x = WatchSyncRefreshCredentialsRequest{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[10] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1076,7 +1590,7 @@ func (x *WatchSyncRefreshCredentialsRequest) String() string { func (*WatchSyncRefreshCredentialsRequest) ProtoMessage() {} func (x *WatchSyncRefreshCredentialsRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[10] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1089,7 +1603,7 @@ func (x *WatchSyncRefreshCredentialsRequest) ProtoReflect() protoreflect.Message // Deprecated: Use WatchSyncRefreshCredentialsRequest.ProtoReflect.Descriptor instead. func (*WatchSyncRefreshCredentialsRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{10} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{14} } func (x *WatchSyncRefreshCredentialsRequest) GetContext() *WatchSyncAuthenticatedContext { @@ -1114,7 +1628,7 @@ type WatchSyncCredentialResponse struct { func (x *WatchSyncCredentialResponse) Reset() { *x = WatchSyncCredentialResponse{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[11] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1126,7 +1640,7 @@ func (x *WatchSyncCredentialResponse) String() string { func (*WatchSyncCredentialResponse) ProtoMessage() {} func (x *WatchSyncCredentialResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[11] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1139,7 +1653,7 @@ func (x *WatchSyncCredentialResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncCredentialResponse.ProtoReflect.Descriptor instead. func (*WatchSyncCredentialResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{11} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{15} } func (x *WatchSyncCredentialResponse) GetCredentials() *WatchSyncCredentials { @@ -1172,7 +1686,7 @@ type WatchSyncGetAccountRequest struct { func (x *WatchSyncGetAccountRequest) Reset() { *x = WatchSyncGetAccountRequest{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[12] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1184,7 +1698,7 @@ func (x *WatchSyncGetAccountRequest) String() string { func (*WatchSyncGetAccountRequest) ProtoMessage() {} func (x *WatchSyncGetAccountRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[12] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1197,7 +1711,7 @@ func (x *WatchSyncGetAccountRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncGetAccountRequest.ProtoReflect.Descriptor instead. func (*WatchSyncGetAccountRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{12} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{16} } func (x *WatchSyncGetAccountRequest) GetContext() *WatchSyncAuthenticatedContext { @@ -1217,7 +1731,7 @@ type WatchSyncGetAccountResponse struct { func (x *WatchSyncGetAccountResponse) Reset() { *x = WatchSyncGetAccountResponse{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[13] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1229,7 +1743,7 @@ func (x *WatchSyncGetAccountResponse) String() string { func (*WatchSyncGetAccountResponse) ProtoMessage() {} func (x *WatchSyncGetAccountResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[13] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1242,7 +1756,7 @@ func (x *WatchSyncGetAccountResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncGetAccountResponse.ProtoReflect.Descriptor instead. func (*WatchSyncGetAccountResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{13} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{17} } func (x *WatchSyncGetAccountResponse) GetAccount() *WatchSyncAccount { @@ -1282,7 +1796,7 @@ type WatchSyncMedia struct { func (x *WatchSyncMedia) Reset() { *x = WatchSyncMedia{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[14] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1294,7 +1808,7 @@ func (x *WatchSyncMedia) String() string { func (*WatchSyncMedia) ProtoMessage() {} func (x *WatchSyncMedia) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[14] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1307,7 +1821,7 @@ func (x *WatchSyncMedia) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncMedia.ProtoReflect.Descriptor instead. func (*WatchSyncMedia) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{14} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{18} } func (x *WatchSyncMedia) GetMediaItemId() string { @@ -1429,13 +1943,20 @@ type WatchSyncEvent struct { DurationSeconds float64 `protobuf:"fixed64,9,opt,name=duration_seconds,json=durationSeconds,proto3" json:"duration_seconds,omitempty"` CompletionPercent float64 `protobuf:"fixed64,10,opt,name=completion_percent,json=completionPercent,proto3" json:"completion_percent,omitempty"` PlayCount int32 `protobuf:"varint,11,opt,name=play_count,json=playCount,proto3" json:"play_count,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Optional zero-based position for list providers that accept explicit + // ordering. Providers may ignore it unless their API supports order writes. + ListPosition *int32 `protobuf:"varint,12,opt,name=list_position,json=listPosition,proto3,oneof" json:"list_position,omitempty"` + // Provider-native key previously returned by ListRemoteState. It lets a + // removal address the exact upstream record when external IDs are absent or + // no longer resolve. + ProviderItemKey string `protobuf:"bytes,13,opt,name=provider_item_key,json=providerItemKey,proto3" json:"provider_item_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *WatchSyncEvent) Reset() { *x = WatchSyncEvent{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[15] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1447,7 +1968,7 @@ func (x *WatchSyncEvent) String() string { func (*WatchSyncEvent) ProtoMessage() {} func (x *WatchSyncEvent) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[15] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1460,7 +1981,7 @@ func (x *WatchSyncEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncEvent.ProtoReflect.Descriptor instead. func (*WatchSyncEvent) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{15} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{19} } func (x *WatchSyncEvent) GetEventId() string { @@ -1540,6 +2061,20 @@ func (x *WatchSyncEvent) GetPlayCount() int32 { return 0 } +func (x *WatchSyncEvent) GetListPosition() int32 { + if x != nil && x.ListPosition != nil { + return *x.ListPosition + } + return 0 +} + +func (x *WatchSyncEvent) GetProviderItemKey() string { + if x != nil { + return x.ProviderItemKey + } + return "" +} + type WatchSyncApplyEventsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Context *WatchSyncAuthenticatedContext `protobuf:"bytes,1,opt,name=context,proto3" json:"context,omitempty"` @@ -1552,7 +2087,7 @@ type WatchSyncApplyEventsRequest struct { func (x *WatchSyncApplyEventsRequest) Reset() { *x = WatchSyncApplyEventsRequest{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[16] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1564,7 +2099,7 @@ func (x *WatchSyncApplyEventsRequest) String() string { func (*WatchSyncApplyEventsRequest) ProtoMessage() {} func (x *WatchSyncApplyEventsRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[16] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1577,7 +2112,7 @@ func (x *WatchSyncApplyEventsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncApplyEventsRequest.ProtoReflect.Descriptor instead. func (*WatchSyncApplyEventsRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{16} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{20} } func (x *WatchSyncApplyEventsRequest) GetContext() *WatchSyncAuthenticatedContext { @@ -1609,7 +2144,7 @@ type WatchSyncApplyResult struct { func (x *WatchSyncApplyResult) Reset() { *x = WatchSyncApplyResult{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[17] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1621,7 +2156,7 @@ func (x *WatchSyncApplyResult) String() string { func (*WatchSyncApplyResult) ProtoMessage() {} func (x *WatchSyncApplyResult) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[17] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1634,7 +2169,7 @@ func (x *WatchSyncApplyResult) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncApplyResult.ProtoReflect.Descriptor instead. func (*WatchSyncApplyResult) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{17} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{21} } func (x *WatchSyncApplyResult) GetEventId() string { @@ -1673,7 +2208,7 @@ type WatchSyncApplyEventsResponse struct { func (x *WatchSyncApplyEventsResponse) Reset() { *x = WatchSyncApplyEventsResponse{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[18] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1685,7 +2220,7 @@ func (x *WatchSyncApplyEventsResponse) String() string { func (*WatchSyncApplyEventsResponse) ProtoMessage() {} func (x *WatchSyncApplyEventsResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[18] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1698,7 +2233,7 @@ func (x *WatchSyncApplyEventsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncApplyEventsResponse.ProtoReflect.Descriptor instead. func (*WatchSyncApplyEventsResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{18} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{22} } func (x *WatchSyncApplyEventsResponse) GetResults() []*WatchSyncApplyResult { @@ -1734,14 +2269,17 @@ type WatchSyncListRemoteStateRequest struct { // after the traversal completes or fails. PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` // Requested maximum number of items. Plugins may return fewer. - PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // State families requested by the host. Empty means every family advertised + // by the descriptor, preserving compatibility with v0.12 hosts and plugins. + StateKinds []WatchSyncRemoteStateKind `protobuf:"varint,5,rep,packed,name=state_kinds,json=stateKinds,proto3,enum=silo.plugin.v1.WatchSyncRemoteStateKind" json:"state_kinds,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *WatchSyncListRemoteStateRequest) Reset() { *x = WatchSyncListRemoteStateRequest{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[19] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1753,7 +2291,7 @@ func (x *WatchSyncListRemoteStateRequest) String() string { func (*WatchSyncListRemoteStateRequest) ProtoMessage() {} func (x *WatchSyncListRemoteStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[19] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1766,7 +2304,7 @@ func (x *WatchSyncListRemoteStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncListRemoteStateRequest.ProtoReflect.Descriptor instead. func (*WatchSyncListRemoteStateRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{19} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{23} } func (x *WatchSyncListRemoteStateRequest) GetContext() *WatchSyncAuthenticatedContext { @@ -1797,6 +2335,13 @@ func (x *WatchSyncListRemoteStateRequest) GetPageSize() int32 { return 0 } +func (x *WatchSyncListRemoteStateRequest) GetStateKinds() []WatchSyncRemoteStateKind { + if x != nil { + return x.StateKinds + } + return nil +} + type WatchSyncRemoteWatchedState struct { state protoimpl.MessageState `protogen:"open.v1"` // Number of completed plays known to the provider. Must be at least one when @@ -1810,7 +2355,7 @@ type WatchSyncRemoteWatchedState struct { func (x *WatchSyncRemoteWatchedState) Reset() { *x = WatchSyncRemoteWatchedState{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[20] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1822,7 +2367,7 @@ func (x *WatchSyncRemoteWatchedState) String() string { func (*WatchSyncRemoteWatchedState) ProtoMessage() {} func (x *WatchSyncRemoteWatchedState) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[20] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1835,7 +2380,7 @@ func (x *WatchSyncRemoteWatchedState) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncRemoteWatchedState.ProtoReflect.Descriptor instead. func (*WatchSyncRemoteWatchedState) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{20} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{24} } func (x *WatchSyncRemoteWatchedState) GetPlayCount() int32 { @@ -1864,7 +2409,7 @@ type WatchSyncRemoteProgressState struct { func (x *WatchSyncRemoteProgressState) Reset() { *x = WatchSyncRemoteProgressState{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[21] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1876,7 +2421,7 @@ func (x *WatchSyncRemoteProgressState) String() string { func (*WatchSyncRemoteProgressState) ProtoMessage() {} func (x *WatchSyncRemoteProgressState) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[21] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1889,7 +2434,7 @@ func (x *WatchSyncRemoteProgressState) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncRemoteProgressState.ProtoReflect.Descriptor instead. func (*WatchSyncRemoteProgressState) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{21} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{25} } func (x *WatchSyncRemoteProgressState) GetProgressPercent() float64 { @@ -1906,21 +2451,80 @@ func (x *WatchSyncRemoteProgressState) GetPausedAt() *timestamppb.Timestamp { return nil } +type WatchSyncRemoteListState struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Provider timestamp for when the item was added, when available. + ListedAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=listed_at,json=listedAt,proto3" json:"listed_at,omitempty"` + // True is an explicit tombstone. It is meaningful for incremental + // traversals, where omitting an item is not a deletion. + Removed bool `protobuf:"varint,2,opt,name=removed,proto3" json:"removed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WatchSyncRemoteListState) Reset() { + *x = WatchSyncRemoteListState{} + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WatchSyncRemoteListState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchSyncRemoteListState) ProtoMessage() {} + +func (x *WatchSyncRemoteListState) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchSyncRemoteListState.ProtoReflect.Descriptor instead. +func (*WatchSyncRemoteListState) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{26} +} + +func (x *WatchSyncRemoteListState) GetListedAt() *timestamppb.Timestamp { + if x != nil { + return x.ListedAt + } + return nil +} + +func (x *WatchSyncRemoteListState) GetRemoved() bool { + if x != nil { + return x.Removed + } + return false +} + type WatchSyncRemoteState struct { state protoimpl.MessageState `protogen:"open.v1"` ProviderItemKey string `protobuf:"bytes,1,opt,name=provider_item_key,json=providerItemKey,proto3" json:"provider_item_key,omitempty"` - Media *WatchSyncMedia `protobuf:"bytes,2,opt,name=media,proto3" json:"media,omitempty"` + // Required except on an explicit favorite/watchlist tombstone whose + // provider_item_key identifies a record returned previously. + Media *WatchSyncMedia `protobuf:"bytes,2,opt,name=media,proto3" json:"media,omitempty"` // At least one typed state must be present. Both may be present when the // provider reports a completed play and a separate resume point. Watched *WatchSyncRemoteWatchedState `protobuf:"bytes,3,opt,name=watched,proto3" json:"watched,omitempty"` Progress *WatchSyncRemoteProgressState `protobuf:"bytes,4,opt,name=progress,proto3" json:"progress,omitempty"` + Favorite *WatchSyncRemoteListState `protobuf:"bytes,5,opt,name=favorite,proto3" json:"favorite,omitempty"` + Watchlist *WatchSyncRemoteListState `protobuf:"bytes,6,opt,name=watchlist,proto3" json:"watchlist,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *WatchSyncRemoteState) Reset() { *x = WatchSyncRemoteState{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[22] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1932,7 +2536,7 @@ func (x *WatchSyncRemoteState) String() string { func (*WatchSyncRemoteState) ProtoMessage() {} func (x *WatchSyncRemoteState) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[22] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1945,7 +2549,7 @@ func (x *WatchSyncRemoteState) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncRemoteState.ProtoReflect.Descriptor instead. func (*WatchSyncRemoteState) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{22} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{27} } func (x *WatchSyncRemoteState) GetProviderItemKey() string { @@ -1976,6 +2580,20 @@ func (x *WatchSyncRemoteState) GetProgress() *WatchSyncRemoteProgressState { return nil } +func (x *WatchSyncRemoteState) GetFavorite() *WatchSyncRemoteListState { + if x != nil { + return x.Favorite + } + return nil +} + +func (x *WatchSyncRemoteState) GetWatchlist() *WatchSyncRemoteListState { + if x != nil { + return x.Watchlist + } + return nil +} + type WatchSyncListRemoteStateResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Items []*WatchSyncRemoteState `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` @@ -1987,7 +2605,9 @@ type WatchSyncListRemoteStateResponse struct { NextCursor string `protobuf:"bytes,3,opt,name=next_cursor,json=nextCursor,proto3" json:"next_cursor,omitempty"` // True when this traversal is an authoritative full snapshot. False means an // incremental delta, so an absent item must not be interpreted as deletion. - // The value must remain stable across every page in one traversal. + // The value must remain stable across every page in one traversal. A provider + // advertising provides_watchlist_order must set this true for WATCHLIST + // traversals so the returned order is unambiguous. CompleteSnapshot bool `protobuf:"varint,4,opt,name=complete_snapshot,json=completeSnapshot,proto3" json:"complete_snapshot,omitempty"` // Complete authoritative replacement. The host persists it before consuming // this page or fault; a persistence failure discards the page and cursor. @@ -2002,7 +2622,7 @@ type WatchSyncListRemoteStateResponse struct { func (x *WatchSyncListRemoteStateResponse) Reset() { *x = WatchSyncListRemoteStateResponse{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[23] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2014,7 +2634,7 @@ func (x *WatchSyncListRemoteStateResponse) String() string { func (*WatchSyncListRemoteStateResponse) ProtoMessage() {} func (x *WatchSyncListRemoteStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[23] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2027,7 +2647,7 @@ func (x *WatchSyncListRemoteStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncListRemoteStateResponse.ProtoReflect.Descriptor instead. func (*WatchSyncListRemoteStateResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{23} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{28} } func (x *WatchSyncListRemoteStateResponse) GetItems() []*WatchSyncRemoteState { @@ -2076,7 +2696,7 @@ var File_silo_plugin_v1_watch_sync_provider_proto protoreflect.FileDescriptor const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\n" + - "(silo/plugin/v1/watch_sync_provider.proto\x12\x0esilo.plugin.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xbb\x03\n" + + "(silo/plugin/v1/watch_sync_provider.proto\x12\x0esilo.plugin.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xa4\x06\n" + "\x1bWatchSyncProviderDescriptor\x12F\n" + "\fauth_methods\x18\x01 \x03(\x0e2#.silo.plugin.v1.WatchSyncAuthMethodR\vauthMethods\x12%\n" + "\x0eexport_watched\x18\x02 \x01(\bR\rexportWatched\x12)\n" + @@ -2085,7 +2705,16 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\x0fimport_progress\x18\x05 \x01(\bR\x0eimportProgress\x12V\n" + "\x15supported_media_types\x18\x06 \x03(\x0e2\".silo.plugin.v1.WatchSyncMediaTypeR\x13supportedMediaTypes\x124\n" + "\x16external_id_namespaces\x18\a \x03(\tR\x14externalIdNamespaces\x12$\n" + - "\x0emax_batch_size\x18\b \x01(\x05R\fmaxBatchSize\"\xc2\x02\n" + + "\x0emax_batch_size\x18\b \x01(\x05R\fmaxBatchSize\x12)\n" + + "\x10import_favorites\x18\t \x01(\bR\x0fimportFavorites\x12)\n" + + "\x10export_favorites\x18\n" + + " \x01(\bR\x0fexportFavorites\x12)\n" + + "\x10remove_favorites\x18\v \x01(\bR\x0fremoveFavorites\x12)\n" + + "\x10import_watchlist\x18\f \x01(\bR\x0fimportWatchlist\x12)\n" + + "\x10export_watchlist\x18\r \x01(\bR\x0fexportWatchlist\x12)\n" + + "\x10remove_watchlist\x18\x0e \x01(\bR\x0fremoveWatchlist\x128\n" + + "\x18provides_watchlist_order\x18\x0f \x01(\bR\x16providesWatchlistOrder\x12+\n" + + "\x11scrobble_playback\x18\x10 \x01(\bR\x10scrobblePlayback\"\xc2\x02\n" + "\x17WatchSyncProviderConfig\x12K\n" + "\x06values\x18\x01 \x03(\v23.silo.plugin.v1.WatchSyncProviderConfig.ValuesEntryR\x06values\x12^\n" + "\rsecret_values\x18\x02 \x03(\v29.silo.plugin.v1.WatchSyncProviderConfig.SecretValuesEntryR\fsecretValues\x1a9\n" + @@ -2142,7 +2771,33 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\x1eWatchSyncExchangeAPIKeyRequest\x12#\n" + "\rcapability_id\x18\x01 \x01(\tR\fcapabilityId\x12P\n" + "\x0fprovider_config\x18\x02 \x01(\v2'.silo.plugin.v1.WatchSyncProviderConfigR\x0eproviderConfig\x12\x17\n" + - "\aapi_key\x18\x03 \x01(\tR\x06apiKey\"m\n" + + "\aapi_key\x18\x03 \x01(\tR\x06apiKey\"\xa8\x01\n" + + "/WatchSyncDeviceAuthorizationServiceStartRequest\x12#\n" + + "\rcapability_id\x18\x01 \x01(\tR\fcapabilityId\x12P\n" + + "\x0fprovider_config\x18\x02 \x01(\v2'.silo.plugin.v1.WatchSyncProviderConfigR\x0eproviderConfig\"\x94\x03\n" + + "0WatchSyncDeviceAuthorizationServiceStartResponse\x12\x1b\n" + + "\tuser_code\x18\x01 \x01(\tR\buserCode\x12)\n" + + "\x10verification_url\x18\x02 \x01(\tR\x0fverificationUrl\x12:\n" + + "\x19verification_url_complete\x18\x03 \x01(\tR\x17verificationUrlComplete\x12%\n" + + "\x0eprovider_state\x18\x04 \x01(\fR\rproviderState\x12D\n" + + "\x10polling_interval\x18\x05 \x01(\v2\x19.google.protobuf.DurationR\x0fpollingInterval\x129\n" + + "\n" + + "expires_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x124\n" + + "\x05fault\x18\a \x01(\v2\x1e.silo.plugin.v1.WatchSyncFaultR\x05fault\"\xce\x01\n" + + ".WatchSyncDeviceAuthorizationServicePollRequest\x12#\n" + + "\rcapability_id\x18\x01 \x01(\tR\fcapabilityId\x12P\n" + + "\x0fprovider_config\x18\x02 \x01(\v2'.silo.plugin.v1.WatchSyncProviderConfigR\x0eproviderConfig\x12%\n" + + "\x0eprovider_state\x18\x03 \x01(\fR\rproviderState\"\xf7\x03\n" + + "/WatchSyncDeviceAuthorizationServicePollResponse\x12J\n" + + "\x06status\x18\x01 \x01(\x0e22.silo.plugin.v1.WatchSyncDeviceAuthorizationStatusR\x06status\x12F\n" + + "\vcredentials\x18\x02 \x01(\v2$.silo.plugin.v1.WatchSyncCredentialsR\vcredentials\x12:\n" + + "\aaccount\x18\x03 \x01(\v2 .silo.plugin.v1.WatchSyncAccountR\aaccount\x124\n" + + "\x05fault\x18\x04 \x01(\v2\x1e.silo.plugin.v1.WatchSyncFaultR\x05fault\x12*\n" + + "\x0eprovider_state\x18\x05 \x01(\fH\x00R\rproviderState\x88\x01\x01\x12D\n" + + "\x10polling_interval\x18\x06 \x01(\v2\x19.google.protobuf.DurationR\x0fpollingInterval\x129\n" + + "\n" + + "expires_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAtB\x11\n" + + "\x0f_provider_state\"m\n" + "\"WatchSyncRefreshCredentialsRequest\x12G\n" + "\acontext\x18\x01 \x01(\v2-.silo.plugin.v1.WatchSyncAuthenticatedContextR\acontext\"\xd7\x01\n" + "\x1bWatchSyncCredentialResponse\x12F\n" + @@ -2178,7 +2833,7 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aD\n" + "\x16SeriesExternalIdsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x97\x04\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xff\x04\n" + "\x0eWatchSyncEvent\x12\x19\n" + "\bevent_id\x18\x01 \x01(\tR\aeventId\x12@\n" + "\toperation\x18\x02 \x01(\x0e2\".silo.plugin.v1.WatchSyncOperationR\toperation\x127\n" + @@ -2193,7 +2848,10 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\x12completion_percent\x18\n" + " \x01(\x01R\x11completionPercent\x12\x1d\n" + "\n" + - "play_count\x18\v \x01(\x05R\tplayCount\"\x9e\x01\n" + + "play_count\x18\v \x01(\x05R\tplayCount\x12(\n" + + "\rlist_position\x18\f \x01(\x05H\x00R\flistPosition\x88\x01\x01\x12*\n" + + "\x11provider_item_key\x18\r \x01(\tR\x0fproviderItemKeyB\x10\n" + + "\x0e_list_position\"\x9e\x01\n" + "\x1bWatchSyncApplyEventsRequest\x12G\n" + "\acontext\x18\x01 \x01(\v2-.silo.plugin.v1.WatchSyncAuthenticatedContextR\acontext\x126\n" + "\x06events\x18\x02 \x03(\v2\x1e.silo.plugin.v1.WatchSyncEventR\x06events\"\xa5\x01\n" + @@ -2204,25 +2862,32 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\x1cWatchSyncApplyEventsResponse\x12>\n" + "\aresults\x18\x01 \x03(\v2$.silo.plugin.v1.WatchSyncApplyResultR\aresults\x12U\n" + "\x13updated_credentials\x18\x02 \x01(\v2$.silo.plugin.v1.WatchSyncCredentialsR\x12updatedCredentials\x124\n" + - "\x05fault\x18\x03 \x01(\v2\x1e.silo.plugin.v1.WatchSyncFaultR\x05fault\"\xbe\x01\n" + + "\x05fault\x18\x03 \x01(\v2\x1e.silo.plugin.v1.WatchSyncFaultR\x05fault\"\x89\x02\n" + "\x1fWatchSyncListRemoteStateRequest\x12G\n" + "\acontext\x18\x01 \x01(\v2-.silo.plugin.v1.WatchSyncAuthenticatedContextR\acontext\x12\x16\n" + "\x06cursor\x18\x02 \x01(\tR\x06cursor\x12\x1d\n" + "\n" + "page_token\x18\x03 \x01(\tR\tpageToken\x12\x1b\n" + - "\tpage_size\x18\x04 \x01(\x05R\bpageSize\"\x80\x01\n" + + "\tpage_size\x18\x04 \x01(\x05R\bpageSize\x12I\n" + + "\vstate_kinds\x18\x05 \x03(\x0e2(.silo.plugin.v1.WatchSyncRemoteStateKindR\n" + + "stateKinds\"\x80\x01\n" + "\x1bWatchSyncRemoteWatchedState\x12\x1d\n" + "\n" + "play_count\x18\x01 \x01(\x05R\tplayCount\x12B\n" + "\x0flast_watched_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\rlastWatchedAt\"\x82\x01\n" + "\x1cWatchSyncRemoteProgressState\x12)\n" + "\x10progress_percent\x18\x01 \x01(\x01R\x0fprogressPercent\x127\n" + - "\tpaused_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\bpausedAt\"\x89\x02\n" + + "\tpaused_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\bpausedAt\"m\n" + + "\x18WatchSyncRemoteListState\x127\n" + + "\tlisted_at\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\blistedAt\x12\x18\n" + + "\aremoved\x18\x02 \x01(\bR\aremoved\"\x97\x03\n" + "\x14WatchSyncRemoteState\x12*\n" + "\x11provider_item_key\x18\x01 \x01(\tR\x0fproviderItemKey\x124\n" + "\x05media\x18\x02 \x01(\v2\x1e.silo.plugin.v1.WatchSyncMediaR\x05media\x12E\n" + "\awatched\x18\x03 \x01(\v2+.silo.plugin.v1.WatchSyncRemoteWatchedStateR\awatched\x12H\n" + - "\bprogress\x18\x04 \x01(\v2,.silo.plugin.v1.WatchSyncRemoteProgressStateR\bprogress\"\xe1\x02\n" + + "\bprogress\x18\x04 \x01(\v2,.silo.plugin.v1.WatchSyncRemoteProgressStateR\bprogress\x12D\n" + + "\bfavorite\x18\x05 \x01(\v2(.silo.plugin.v1.WatchSyncRemoteListStateR\bfavorite\x12F\n" + + "\twatchlist\x18\x06 \x01(\v2(.silo.plugin.v1.WatchSyncRemoteListStateR\twatchlist\"\xe1\x02\n" + " WatchSyncListRemoteStateResponse\x12:\n" + "\x05items\x18\x01 \x03(\v2$.silo.plugin.v1.WatchSyncRemoteStateR\x05items\x12&\n" + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1f\n" + @@ -2230,19 +2895,39 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "nextCursor\x12+\n" + "\x11complete_snapshot\x18\x04 \x01(\bR\x10completeSnapshot\x12U\n" + "\x13updated_credentials\x18\x05 \x01(\v2$.silo.plugin.v1.WatchSyncCredentialsR\x12updatedCredentials\x124\n" + - "\x05fault\x18\x06 \x01(\v2\x1e.silo.plugin.v1.WatchSyncFaultR\x05fault*\x90\x01\n" + + "\x05fault\x18\x06 \x01(\v2\x1e.silo.plugin.v1.WatchSyncFaultR\x05fault*\xb8\x01\n" + "\x13WatchSyncAuthMethod\x12&\n" + "\"WATCH_SYNC_AUTH_METHOD_UNSPECIFIED\x10\x00\x12-\n" + ")WATCH_SYNC_AUTH_METHOD_AUTHORIZATION_CODE\x10\x01\x12\"\n" + - "\x1eWATCH_SYNC_AUTH_METHOD_API_KEY\x10\x02*\x7f\n" + + "\x1eWATCH_SYNC_AUTH_METHOD_API_KEY\x10\x02\x12&\n" + + "\"WATCH_SYNC_AUTH_METHOD_DEVICE_CODE\x10\x03*\x7f\n" + "\x12WatchSyncMediaType\x12%\n" + "!WATCH_SYNC_MEDIA_TYPE_UNSPECIFIED\x10\x00\x12\x1f\n" + "\x1bWATCH_SYNC_MEDIA_TYPE_MOVIE\x10\x01\x12!\n" + - "\x1dWATCH_SYNC_MEDIA_TYPE_EPISODE\x10\x02*\x8a\x01\n" + + "\x1dWATCH_SYNC_MEDIA_TYPE_EPISODE\x10\x02*\xb0\x03\n" + "\x12WatchSyncOperation\x12$\n" + " WATCH_SYNC_OPERATION_UNSPECIFIED\x10\x00\x12%\n" + "!WATCH_SYNC_OPERATION_MARK_WATCHED\x10\x01\x12'\n" + - "#WATCH_SYNC_OPERATION_MARK_UNWATCHED\x10\x02*\xa3\x01\n" + + "#WATCH_SYNC_OPERATION_MARK_UNWATCHED\x10\x02\x12%\n" + + "!WATCH_SYNC_OPERATION_ADD_FAVORITE\x10\x03\x12(\n" + + "$WATCH_SYNC_OPERATION_REMOVE_FAVORITE\x10\x04\x12)\n" + + "%WATCH_SYNC_OPERATION_ADD_TO_WATCHLIST\x10\x05\x12.\n" + + "*WATCH_SYNC_OPERATION_REMOVE_FROM_WATCHLIST\x10\x06\x12'\n" + + "#WATCH_SYNC_OPERATION_SCROBBLE_START\x10\a\x12'\n" + + "#WATCH_SYNC_OPERATION_SCROBBLE_PAUSE\x10\b\x12&\n" + + "\"WATCH_SYNC_OPERATION_SCROBBLE_STOP\x10\t*\xf4\x01\n" + + "\x18WatchSyncRemoteStateKind\x12,\n" + + "(WATCH_SYNC_REMOTE_STATE_KIND_UNSPECIFIED\x10\x00\x12(\n" + + "$WATCH_SYNC_REMOTE_STATE_KIND_WATCHED\x10\x01\x12)\n" + + "%WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS\x10\x02\x12)\n" + + "%WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE\x10\x03\x12*\n" + + "&WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST\x10\x04*\xae\x02\n" + + "\"WatchSyncDeviceAuthorizationStatus\x126\n" + + "2WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_UNSPECIFIED\x10\x00\x122\n" + + ".WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_PENDING\x10\x01\x125\n" + + "1WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_AUTHORIZED\x10\x02\x121\n" + + "-WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_DENIED\x10\x03\x122\n" + + ".WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_EXPIRED\x10\x04*\xa3\x01\n" + "\x0fWatchSyncOrigin\x12!\n" + "\x1dWATCH_SYNC_ORIGIN_UNSPECIFIED\x10\x00\x12)\n" + "%WATCH_SYNC_ORIGIN_PLAYBACK_COMPLETION\x10\x01\x12\x1c\n" + @@ -2270,7 +2955,10 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\n" + "GetAccount\x12*.silo.plugin.v1.WatchSyncGetAccountRequest\x1a+.silo.plugin.v1.WatchSyncGetAccountResponse\x12h\n" + "\vApplyEvents\x12+.silo.plugin.v1.WatchSyncApplyEventsRequest\x1a,.silo.plugin.v1.WatchSyncApplyEventsResponse\x12t\n" + - "\x0fListRemoteState\x12/.silo.plugin.v1.WatchSyncListRemoteStateRequest\x1a0.silo.plugin.v1.WatchSyncListRemoteStateResponseBPZNgithub.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1;pluginv1b\x06proto3" + "\x0fListRemoteState\x12/.silo.plugin.v1.WatchSyncListRemoteStateRequest\x1a0.silo.plugin.v1.WatchSyncListRemoteStateResponse2\xbc\x02\n" + + "#WatchSyncDeviceAuthorizationService\x12\x8a\x01\n" + + "\x05Start\x12?.silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartRequest\x1a@.silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse\x12\x87\x01\n" + + "\x04Poll\x12>.silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollRequest\x1a?.silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponseBPZNgithub.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1;pluginv1b\x06proto3" var ( file_silo_plugin_v1_watch_sync_provider_proto_rawDescOnce sync.Once @@ -2284,113 +2972,139 @@ func file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP() []byte { return file_silo_plugin_v1_watch_sync_provider_proto_rawDescData } -var file_silo_plugin_v1_watch_sync_provider_proto_enumTypes = make([]protoimpl.EnumInfo, 6) -var file_silo_plugin_v1_watch_sync_provider_proto_msgTypes = make([]protoimpl.MessageInfo, 29) +var file_silo_plugin_v1_watch_sync_provider_proto_enumTypes = make([]protoimpl.EnumInfo, 8) +var file_silo_plugin_v1_watch_sync_provider_proto_msgTypes = make([]protoimpl.MessageInfo, 34) var file_silo_plugin_v1_watch_sync_provider_proto_goTypes = []any{ - (WatchSyncAuthMethod)(0), // 0: silo.plugin.v1.WatchSyncAuthMethod - (WatchSyncMediaType)(0), // 1: silo.plugin.v1.WatchSyncMediaType - (WatchSyncOperation)(0), // 2: silo.plugin.v1.WatchSyncOperation - (WatchSyncOrigin)(0), // 3: silo.plugin.v1.WatchSyncOrigin - (WatchSyncApplyStatus)(0), // 4: silo.plugin.v1.WatchSyncApplyStatus - (WatchSyncFaultCode)(0), // 5: silo.plugin.v1.WatchSyncFaultCode - (*WatchSyncProviderDescriptor)(nil), // 6: silo.plugin.v1.WatchSyncProviderDescriptor - (*WatchSyncProviderConfig)(nil), // 7: silo.plugin.v1.WatchSyncProviderConfig - (*WatchSyncCredentials)(nil), // 8: silo.plugin.v1.WatchSyncCredentials - (*WatchSyncAuthenticatedContext)(nil), // 9: silo.plugin.v1.WatchSyncAuthenticatedContext - (*WatchSyncAccount)(nil), // 10: silo.plugin.v1.WatchSyncAccount - (*WatchSyncFault)(nil), // 11: silo.plugin.v1.WatchSyncFault - (*WatchSyncInitAuthorizeRequest)(nil), // 12: silo.plugin.v1.WatchSyncInitAuthorizeRequest - (*WatchSyncInitAuthorizeResponse)(nil), // 13: silo.plugin.v1.WatchSyncInitAuthorizeResponse - (*WatchSyncExchangeCodeRequest)(nil), // 14: silo.plugin.v1.WatchSyncExchangeCodeRequest - (*WatchSyncExchangeAPIKeyRequest)(nil), // 15: silo.plugin.v1.WatchSyncExchangeAPIKeyRequest - (*WatchSyncRefreshCredentialsRequest)(nil), // 16: silo.plugin.v1.WatchSyncRefreshCredentialsRequest - (*WatchSyncCredentialResponse)(nil), // 17: silo.plugin.v1.WatchSyncCredentialResponse - (*WatchSyncGetAccountRequest)(nil), // 18: silo.plugin.v1.WatchSyncGetAccountRequest - (*WatchSyncGetAccountResponse)(nil), // 19: silo.plugin.v1.WatchSyncGetAccountResponse - (*WatchSyncMedia)(nil), // 20: silo.plugin.v1.WatchSyncMedia - (*WatchSyncEvent)(nil), // 21: silo.plugin.v1.WatchSyncEvent - (*WatchSyncApplyEventsRequest)(nil), // 22: silo.plugin.v1.WatchSyncApplyEventsRequest - (*WatchSyncApplyResult)(nil), // 23: silo.plugin.v1.WatchSyncApplyResult - (*WatchSyncApplyEventsResponse)(nil), // 24: silo.plugin.v1.WatchSyncApplyEventsResponse - (*WatchSyncListRemoteStateRequest)(nil), // 25: silo.plugin.v1.WatchSyncListRemoteStateRequest - (*WatchSyncRemoteWatchedState)(nil), // 26: silo.plugin.v1.WatchSyncRemoteWatchedState - (*WatchSyncRemoteProgressState)(nil), // 27: silo.plugin.v1.WatchSyncRemoteProgressState - (*WatchSyncRemoteState)(nil), // 28: silo.plugin.v1.WatchSyncRemoteState - (*WatchSyncListRemoteStateResponse)(nil), // 29: silo.plugin.v1.WatchSyncListRemoteStateResponse - nil, // 30: silo.plugin.v1.WatchSyncProviderConfig.ValuesEntry - nil, // 31: silo.plugin.v1.WatchSyncProviderConfig.SecretValuesEntry - nil, // 32: silo.plugin.v1.WatchSyncCredentials.SecretAttributesEntry - nil, // 33: silo.plugin.v1.WatchSyncMedia.ExternalIdsEntry - nil, // 34: silo.plugin.v1.WatchSyncMedia.SeriesExternalIdsEntry - (*timestamppb.Timestamp)(nil), // 35: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 36: google.protobuf.Duration - (*structpb.Struct)(nil), // 37: google.protobuf.Struct + (WatchSyncAuthMethod)(0), // 0: silo.plugin.v1.WatchSyncAuthMethod + (WatchSyncMediaType)(0), // 1: silo.plugin.v1.WatchSyncMediaType + (WatchSyncOperation)(0), // 2: silo.plugin.v1.WatchSyncOperation + (WatchSyncRemoteStateKind)(0), // 3: silo.plugin.v1.WatchSyncRemoteStateKind + (WatchSyncDeviceAuthorizationStatus)(0), // 4: silo.plugin.v1.WatchSyncDeviceAuthorizationStatus + (WatchSyncOrigin)(0), // 5: silo.plugin.v1.WatchSyncOrigin + (WatchSyncApplyStatus)(0), // 6: silo.plugin.v1.WatchSyncApplyStatus + (WatchSyncFaultCode)(0), // 7: silo.plugin.v1.WatchSyncFaultCode + (*WatchSyncProviderDescriptor)(nil), // 8: silo.plugin.v1.WatchSyncProviderDescriptor + (*WatchSyncProviderConfig)(nil), // 9: silo.plugin.v1.WatchSyncProviderConfig + (*WatchSyncCredentials)(nil), // 10: silo.plugin.v1.WatchSyncCredentials + (*WatchSyncAuthenticatedContext)(nil), // 11: silo.plugin.v1.WatchSyncAuthenticatedContext + (*WatchSyncAccount)(nil), // 12: silo.plugin.v1.WatchSyncAccount + (*WatchSyncFault)(nil), // 13: silo.plugin.v1.WatchSyncFault + (*WatchSyncInitAuthorizeRequest)(nil), // 14: silo.plugin.v1.WatchSyncInitAuthorizeRequest + (*WatchSyncInitAuthorizeResponse)(nil), // 15: silo.plugin.v1.WatchSyncInitAuthorizeResponse + (*WatchSyncExchangeCodeRequest)(nil), // 16: silo.plugin.v1.WatchSyncExchangeCodeRequest + (*WatchSyncExchangeAPIKeyRequest)(nil), // 17: silo.plugin.v1.WatchSyncExchangeAPIKeyRequest + (*WatchSyncDeviceAuthorizationServiceStartRequest)(nil), // 18: silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartRequest + (*WatchSyncDeviceAuthorizationServiceStartResponse)(nil), // 19: silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse + (*WatchSyncDeviceAuthorizationServicePollRequest)(nil), // 20: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollRequest + (*WatchSyncDeviceAuthorizationServicePollResponse)(nil), // 21: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse + (*WatchSyncRefreshCredentialsRequest)(nil), // 22: silo.plugin.v1.WatchSyncRefreshCredentialsRequest + (*WatchSyncCredentialResponse)(nil), // 23: silo.plugin.v1.WatchSyncCredentialResponse + (*WatchSyncGetAccountRequest)(nil), // 24: silo.plugin.v1.WatchSyncGetAccountRequest + (*WatchSyncGetAccountResponse)(nil), // 25: silo.plugin.v1.WatchSyncGetAccountResponse + (*WatchSyncMedia)(nil), // 26: silo.plugin.v1.WatchSyncMedia + (*WatchSyncEvent)(nil), // 27: silo.plugin.v1.WatchSyncEvent + (*WatchSyncApplyEventsRequest)(nil), // 28: silo.plugin.v1.WatchSyncApplyEventsRequest + (*WatchSyncApplyResult)(nil), // 29: silo.plugin.v1.WatchSyncApplyResult + (*WatchSyncApplyEventsResponse)(nil), // 30: silo.plugin.v1.WatchSyncApplyEventsResponse + (*WatchSyncListRemoteStateRequest)(nil), // 31: silo.plugin.v1.WatchSyncListRemoteStateRequest + (*WatchSyncRemoteWatchedState)(nil), // 32: silo.plugin.v1.WatchSyncRemoteWatchedState + (*WatchSyncRemoteProgressState)(nil), // 33: silo.plugin.v1.WatchSyncRemoteProgressState + (*WatchSyncRemoteListState)(nil), // 34: silo.plugin.v1.WatchSyncRemoteListState + (*WatchSyncRemoteState)(nil), // 35: silo.plugin.v1.WatchSyncRemoteState + (*WatchSyncListRemoteStateResponse)(nil), // 36: silo.plugin.v1.WatchSyncListRemoteStateResponse + nil, // 37: silo.plugin.v1.WatchSyncProviderConfig.ValuesEntry + nil, // 38: silo.plugin.v1.WatchSyncProviderConfig.SecretValuesEntry + nil, // 39: silo.plugin.v1.WatchSyncCredentials.SecretAttributesEntry + nil, // 40: silo.plugin.v1.WatchSyncMedia.ExternalIdsEntry + nil, // 41: silo.plugin.v1.WatchSyncMedia.SeriesExternalIdsEntry + (*timestamppb.Timestamp)(nil), // 42: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 43: google.protobuf.Duration + (*structpb.Struct)(nil), // 44: google.protobuf.Struct } var file_silo_plugin_v1_watch_sync_provider_proto_depIdxs = []int32{ 0, // 0: silo.plugin.v1.WatchSyncProviderDescriptor.auth_methods:type_name -> silo.plugin.v1.WatchSyncAuthMethod 1, // 1: silo.plugin.v1.WatchSyncProviderDescriptor.supported_media_types:type_name -> silo.plugin.v1.WatchSyncMediaType - 30, // 2: silo.plugin.v1.WatchSyncProviderConfig.values:type_name -> silo.plugin.v1.WatchSyncProviderConfig.ValuesEntry - 31, // 3: silo.plugin.v1.WatchSyncProviderConfig.secret_values:type_name -> silo.plugin.v1.WatchSyncProviderConfig.SecretValuesEntry - 35, // 4: silo.plugin.v1.WatchSyncCredentials.expires_at:type_name -> google.protobuf.Timestamp - 32, // 5: silo.plugin.v1.WatchSyncCredentials.secret_attributes:type_name -> silo.plugin.v1.WatchSyncCredentials.SecretAttributesEntry - 7, // 6: silo.plugin.v1.WatchSyncAuthenticatedContext.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig - 8, // 7: silo.plugin.v1.WatchSyncAuthenticatedContext.credentials:type_name -> silo.plugin.v1.WatchSyncCredentials - 5, // 8: silo.plugin.v1.WatchSyncFault.code:type_name -> silo.plugin.v1.WatchSyncFaultCode - 36, // 9: silo.plugin.v1.WatchSyncFault.retry_after:type_name -> google.protobuf.Duration - 7, // 10: silo.plugin.v1.WatchSyncInitAuthorizeRequest.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig - 11, // 11: silo.plugin.v1.WatchSyncInitAuthorizeResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault - 7, // 12: silo.plugin.v1.WatchSyncExchangeCodeRequest.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig - 7, // 13: silo.plugin.v1.WatchSyncExchangeAPIKeyRequest.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig - 9, // 14: silo.plugin.v1.WatchSyncRefreshCredentialsRequest.context:type_name -> silo.plugin.v1.WatchSyncAuthenticatedContext - 8, // 15: silo.plugin.v1.WatchSyncCredentialResponse.credentials:type_name -> silo.plugin.v1.WatchSyncCredentials - 10, // 16: silo.plugin.v1.WatchSyncCredentialResponse.account:type_name -> silo.plugin.v1.WatchSyncAccount - 11, // 17: silo.plugin.v1.WatchSyncCredentialResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault - 9, // 18: silo.plugin.v1.WatchSyncGetAccountRequest.context:type_name -> silo.plugin.v1.WatchSyncAuthenticatedContext - 10, // 19: silo.plugin.v1.WatchSyncGetAccountResponse.account:type_name -> silo.plugin.v1.WatchSyncAccount - 11, // 20: silo.plugin.v1.WatchSyncGetAccountResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault - 1, // 21: silo.plugin.v1.WatchSyncMedia.media_type:type_name -> silo.plugin.v1.WatchSyncMediaType - 33, // 22: silo.plugin.v1.WatchSyncMedia.external_ids:type_name -> silo.plugin.v1.WatchSyncMedia.ExternalIdsEntry - 34, // 23: silo.plugin.v1.WatchSyncMedia.series_external_ids:type_name -> silo.plugin.v1.WatchSyncMedia.SeriesExternalIdsEntry - 37, // 24: silo.plugin.v1.WatchSyncMedia.metadata:type_name -> google.protobuf.Struct - 2, // 25: silo.plugin.v1.WatchSyncEvent.operation:type_name -> silo.plugin.v1.WatchSyncOperation - 3, // 26: silo.plugin.v1.WatchSyncEvent.origin:type_name -> silo.plugin.v1.WatchSyncOrigin - 35, // 27: silo.plugin.v1.WatchSyncEvent.occurred_at:type_name -> google.protobuf.Timestamp - 20, // 28: silo.plugin.v1.WatchSyncEvent.media:type_name -> silo.plugin.v1.WatchSyncMedia - 9, // 29: silo.plugin.v1.WatchSyncApplyEventsRequest.context:type_name -> silo.plugin.v1.WatchSyncAuthenticatedContext - 21, // 30: silo.plugin.v1.WatchSyncApplyEventsRequest.events:type_name -> silo.plugin.v1.WatchSyncEvent - 4, // 31: silo.plugin.v1.WatchSyncApplyResult.status:type_name -> silo.plugin.v1.WatchSyncApplyStatus - 11, // 32: silo.plugin.v1.WatchSyncApplyResult.fault:type_name -> silo.plugin.v1.WatchSyncFault - 23, // 33: silo.plugin.v1.WatchSyncApplyEventsResponse.results:type_name -> silo.plugin.v1.WatchSyncApplyResult - 8, // 34: silo.plugin.v1.WatchSyncApplyEventsResponse.updated_credentials:type_name -> silo.plugin.v1.WatchSyncCredentials - 11, // 35: silo.plugin.v1.WatchSyncApplyEventsResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault - 9, // 36: silo.plugin.v1.WatchSyncListRemoteStateRequest.context:type_name -> silo.plugin.v1.WatchSyncAuthenticatedContext - 35, // 37: silo.plugin.v1.WatchSyncRemoteWatchedState.last_watched_at:type_name -> google.protobuf.Timestamp - 35, // 38: silo.plugin.v1.WatchSyncRemoteProgressState.paused_at:type_name -> google.protobuf.Timestamp - 20, // 39: silo.plugin.v1.WatchSyncRemoteState.media:type_name -> silo.plugin.v1.WatchSyncMedia - 26, // 40: silo.plugin.v1.WatchSyncRemoteState.watched:type_name -> silo.plugin.v1.WatchSyncRemoteWatchedState - 27, // 41: silo.plugin.v1.WatchSyncRemoteState.progress:type_name -> silo.plugin.v1.WatchSyncRemoteProgressState - 28, // 42: silo.plugin.v1.WatchSyncListRemoteStateResponse.items:type_name -> silo.plugin.v1.WatchSyncRemoteState - 8, // 43: silo.plugin.v1.WatchSyncListRemoteStateResponse.updated_credentials:type_name -> silo.plugin.v1.WatchSyncCredentials - 11, // 44: silo.plugin.v1.WatchSyncListRemoteStateResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault - 12, // 45: silo.plugin.v1.WatchSyncProvider.InitAuthorize:input_type -> silo.plugin.v1.WatchSyncInitAuthorizeRequest - 14, // 46: silo.plugin.v1.WatchSyncProvider.ExchangeCode:input_type -> silo.plugin.v1.WatchSyncExchangeCodeRequest - 15, // 47: silo.plugin.v1.WatchSyncProvider.ExchangeAPIKey:input_type -> silo.plugin.v1.WatchSyncExchangeAPIKeyRequest - 16, // 48: silo.plugin.v1.WatchSyncProvider.RefreshCredentials:input_type -> silo.plugin.v1.WatchSyncRefreshCredentialsRequest - 18, // 49: silo.plugin.v1.WatchSyncProvider.GetAccount:input_type -> silo.plugin.v1.WatchSyncGetAccountRequest - 22, // 50: silo.plugin.v1.WatchSyncProvider.ApplyEvents:input_type -> silo.plugin.v1.WatchSyncApplyEventsRequest - 25, // 51: silo.plugin.v1.WatchSyncProvider.ListRemoteState:input_type -> silo.plugin.v1.WatchSyncListRemoteStateRequest - 13, // 52: silo.plugin.v1.WatchSyncProvider.InitAuthorize:output_type -> silo.plugin.v1.WatchSyncInitAuthorizeResponse - 17, // 53: silo.plugin.v1.WatchSyncProvider.ExchangeCode:output_type -> silo.plugin.v1.WatchSyncCredentialResponse - 17, // 54: silo.plugin.v1.WatchSyncProvider.ExchangeAPIKey:output_type -> silo.plugin.v1.WatchSyncCredentialResponse - 17, // 55: silo.plugin.v1.WatchSyncProvider.RefreshCredentials:output_type -> silo.plugin.v1.WatchSyncCredentialResponse - 19, // 56: silo.plugin.v1.WatchSyncProvider.GetAccount:output_type -> silo.plugin.v1.WatchSyncGetAccountResponse - 24, // 57: silo.plugin.v1.WatchSyncProvider.ApplyEvents:output_type -> silo.plugin.v1.WatchSyncApplyEventsResponse - 29, // 58: silo.plugin.v1.WatchSyncProvider.ListRemoteState:output_type -> silo.plugin.v1.WatchSyncListRemoteStateResponse - 52, // [52:59] is the sub-list for method output_type - 45, // [45:52] is the sub-list for method input_type - 45, // [45:45] is the sub-list for extension type_name - 45, // [45:45] is the sub-list for extension extendee - 0, // [0:45] is the sub-list for field type_name + 37, // 2: silo.plugin.v1.WatchSyncProviderConfig.values:type_name -> silo.plugin.v1.WatchSyncProviderConfig.ValuesEntry + 38, // 3: silo.plugin.v1.WatchSyncProviderConfig.secret_values:type_name -> silo.plugin.v1.WatchSyncProviderConfig.SecretValuesEntry + 42, // 4: silo.plugin.v1.WatchSyncCredentials.expires_at:type_name -> google.protobuf.Timestamp + 39, // 5: silo.plugin.v1.WatchSyncCredentials.secret_attributes:type_name -> silo.plugin.v1.WatchSyncCredentials.SecretAttributesEntry + 9, // 6: silo.plugin.v1.WatchSyncAuthenticatedContext.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig + 10, // 7: silo.plugin.v1.WatchSyncAuthenticatedContext.credentials:type_name -> silo.plugin.v1.WatchSyncCredentials + 7, // 8: silo.plugin.v1.WatchSyncFault.code:type_name -> silo.plugin.v1.WatchSyncFaultCode + 43, // 9: silo.plugin.v1.WatchSyncFault.retry_after:type_name -> google.protobuf.Duration + 9, // 10: silo.plugin.v1.WatchSyncInitAuthorizeRequest.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig + 13, // 11: silo.plugin.v1.WatchSyncInitAuthorizeResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault + 9, // 12: silo.plugin.v1.WatchSyncExchangeCodeRequest.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig + 9, // 13: silo.plugin.v1.WatchSyncExchangeAPIKeyRequest.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig + 9, // 14: silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartRequest.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig + 43, // 15: silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse.polling_interval:type_name -> google.protobuf.Duration + 42, // 16: silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse.expires_at:type_name -> google.protobuf.Timestamp + 13, // 17: silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault + 9, // 18: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollRequest.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig + 4, // 19: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.status:type_name -> silo.plugin.v1.WatchSyncDeviceAuthorizationStatus + 10, // 20: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.credentials:type_name -> silo.plugin.v1.WatchSyncCredentials + 12, // 21: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.account:type_name -> silo.plugin.v1.WatchSyncAccount + 13, // 22: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault + 43, // 23: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.polling_interval:type_name -> google.protobuf.Duration + 42, // 24: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.expires_at:type_name -> google.protobuf.Timestamp + 11, // 25: silo.plugin.v1.WatchSyncRefreshCredentialsRequest.context:type_name -> silo.plugin.v1.WatchSyncAuthenticatedContext + 10, // 26: silo.plugin.v1.WatchSyncCredentialResponse.credentials:type_name -> silo.plugin.v1.WatchSyncCredentials + 12, // 27: silo.plugin.v1.WatchSyncCredentialResponse.account:type_name -> silo.plugin.v1.WatchSyncAccount + 13, // 28: silo.plugin.v1.WatchSyncCredentialResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault + 11, // 29: silo.plugin.v1.WatchSyncGetAccountRequest.context:type_name -> silo.plugin.v1.WatchSyncAuthenticatedContext + 12, // 30: silo.plugin.v1.WatchSyncGetAccountResponse.account:type_name -> silo.plugin.v1.WatchSyncAccount + 13, // 31: silo.plugin.v1.WatchSyncGetAccountResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault + 1, // 32: silo.plugin.v1.WatchSyncMedia.media_type:type_name -> silo.plugin.v1.WatchSyncMediaType + 40, // 33: silo.plugin.v1.WatchSyncMedia.external_ids:type_name -> silo.plugin.v1.WatchSyncMedia.ExternalIdsEntry + 41, // 34: silo.plugin.v1.WatchSyncMedia.series_external_ids:type_name -> silo.plugin.v1.WatchSyncMedia.SeriesExternalIdsEntry + 44, // 35: silo.plugin.v1.WatchSyncMedia.metadata:type_name -> google.protobuf.Struct + 2, // 36: silo.plugin.v1.WatchSyncEvent.operation:type_name -> silo.plugin.v1.WatchSyncOperation + 5, // 37: silo.plugin.v1.WatchSyncEvent.origin:type_name -> silo.plugin.v1.WatchSyncOrigin + 42, // 38: silo.plugin.v1.WatchSyncEvent.occurred_at:type_name -> google.protobuf.Timestamp + 26, // 39: silo.plugin.v1.WatchSyncEvent.media:type_name -> silo.plugin.v1.WatchSyncMedia + 11, // 40: silo.plugin.v1.WatchSyncApplyEventsRequest.context:type_name -> silo.plugin.v1.WatchSyncAuthenticatedContext + 27, // 41: silo.plugin.v1.WatchSyncApplyEventsRequest.events:type_name -> silo.plugin.v1.WatchSyncEvent + 6, // 42: silo.plugin.v1.WatchSyncApplyResult.status:type_name -> silo.plugin.v1.WatchSyncApplyStatus + 13, // 43: silo.plugin.v1.WatchSyncApplyResult.fault:type_name -> silo.plugin.v1.WatchSyncFault + 29, // 44: silo.plugin.v1.WatchSyncApplyEventsResponse.results:type_name -> silo.plugin.v1.WatchSyncApplyResult + 10, // 45: silo.plugin.v1.WatchSyncApplyEventsResponse.updated_credentials:type_name -> silo.plugin.v1.WatchSyncCredentials + 13, // 46: silo.plugin.v1.WatchSyncApplyEventsResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault + 11, // 47: silo.plugin.v1.WatchSyncListRemoteStateRequest.context:type_name -> silo.plugin.v1.WatchSyncAuthenticatedContext + 3, // 48: silo.plugin.v1.WatchSyncListRemoteStateRequest.state_kinds:type_name -> silo.plugin.v1.WatchSyncRemoteStateKind + 42, // 49: silo.plugin.v1.WatchSyncRemoteWatchedState.last_watched_at:type_name -> google.protobuf.Timestamp + 42, // 50: silo.plugin.v1.WatchSyncRemoteProgressState.paused_at:type_name -> google.protobuf.Timestamp + 42, // 51: silo.plugin.v1.WatchSyncRemoteListState.listed_at:type_name -> google.protobuf.Timestamp + 26, // 52: silo.plugin.v1.WatchSyncRemoteState.media:type_name -> silo.plugin.v1.WatchSyncMedia + 32, // 53: silo.plugin.v1.WatchSyncRemoteState.watched:type_name -> silo.plugin.v1.WatchSyncRemoteWatchedState + 33, // 54: silo.plugin.v1.WatchSyncRemoteState.progress:type_name -> silo.plugin.v1.WatchSyncRemoteProgressState + 34, // 55: silo.plugin.v1.WatchSyncRemoteState.favorite:type_name -> silo.plugin.v1.WatchSyncRemoteListState + 34, // 56: silo.plugin.v1.WatchSyncRemoteState.watchlist:type_name -> silo.plugin.v1.WatchSyncRemoteListState + 35, // 57: silo.plugin.v1.WatchSyncListRemoteStateResponse.items:type_name -> silo.plugin.v1.WatchSyncRemoteState + 10, // 58: silo.plugin.v1.WatchSyncListRemoteStateResponse.updated_credentials:type_name -> silo.plugin.v1.WatchSyncCredentials + 13, // 59: silo.plugin.v1.WatchSyncListRemoteStateResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault + 14, // 60: silo.plugin.v1.WatchSyncProvider.InitAuthorize:input_type -> silo.plugin.v1.WatchSyncInitAuthorizeRequest + 16, // 61: silo.plugin.v1.WatchSyncProvider.ExchangeCode:input_type -> silo.plugin.v1.WatchSyncExchangeCodeRequest + 17, // 62: silo.plugin.v1.WatchSyncProvider.ExchangeAPIKey:input_type -> silo.plugin.v1.WatchSyncExchangeAPIKeyRequest + 22, // 63: silo.plugin.v1.WatchSyncProvider.RefreshCredentials:input_type -> silo.plugin.v1.WatchSyncRefreshCredentialsRequest + 24, // 64: silo.plugin.v1.WatchSyncProvider.GetAccount:input_type -> silo.plugin.v1.WatchSyncGetAccountRequest + 28, // 65: silo.plugin.v1.WatchSyncProvider.ApplyEvents:input_type -> silo.plugin.v1.WatchSyncApplyEventsRequest + 31, // 66: silo.plugin.v1.WatchSyncProvider.ListRemoteState:input_type -> silo.plugin.v1.WatchSyncListRemoteStateRequest + 18, // 67: silo.plugin.v1.WatchSyncDeviceAuthorizationService.Start:input_type -> silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartRequest + 20, // 68: silo.plugin.v1.WatchSyncDeviceAuthorizationService.Poll:input_type -> silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollRequest + 15, // 69: silo.plugin.v1.WatchSyncProvider.InitAuthorize:output_type -> silo.plugin.v1.WatchSyncInitAuthorizeResponse + 23, // 70: silo.plugin.v1.WatchSyncProvider.ExchangeCode:output_type -> silo.plugin.v1.WatchSyncCredentialResponse + 23, // 71: silo.plugin.v1.WatchSyncProvider.ExchangeAPIKey:output_type -> silo.plugin.v1.WatchSyncCredentialResponse + 23, // 72: silo.plugin.v1.WatchSyncProvider.RefreshCredentials:output_type -> silo.plugin.v1.WatchSyncCredentialResponse + 25, // 73: silo.plugin.v1.WatchSyncProvider.GetAccount:output_type -> silo.plugin.v1.WatchSyncGetAccountResponse + 30, // 74: silo.plugin.v1.WatchSyncProvider.ApplyEvents:output_type -> silo.plugin.v1.WatchSyncApplyEventsResponse + 36, // 75: silo.plugin.v1.WatchSyncProvider.ListRemoteState:output_type -> silo.plugin.v1.WatchSyncListRemoteStateResponse + 19, // 76: silo.plugin.v1.WatchSyncDeviceAuthorizationService.Start:output_type -> silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse + 21, // 77: silo.plugin.v1.WatchSyncDeviceAuthorizationService.Poll:output_type -> silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse + 69, // [69:78] is the sub-list for method output_type + 60, // [60:69] is the sub-list for method input_type + 60, // [60:60] is the sub-list for extension type_name + 60, // [60:60] is the sub-list for extension extendee + 0, // [0:60] is the sub-list for field type_name } func init() { file_silo_plugin_v1_watch_sync_provider_proto_init() } @@ -2398,15 +3112,17 @@ func file_silo_plugin_v1_watch_sync_provider_proto_init() { if File_silo_plugin_v1_watch_sync_provider_proto != nil { return } + file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[13].OneofWrappers = []any{} + file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[19].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_silo_plugin_v1_watch_sync_provider_proto_rawDesc), len(file_silo_plugin_v1_watch_sync_provider_proto_rawDesc)), - NumEnums: 6, - NumMessages: 29, + NumEnums: 8, + NumMessages: 34, NumExtensions: 0, - NumServices: 1, + NumServices: 2, }, GoTypes: file_silo_plugin_v1_watch_sync_provider_proto_goTypes, DependencyIndexes: file_silo_plugin_v1_watch_sync_provider_proto_depIdxs, diff --git a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_compat_test.go b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_compat_test.go new file mode 100644 index 0000000..e415a1e --- /dev/null +++ b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_compat_test.go @@ -0,0 +1,39 @@ +package pluginv1 + +import "context" + +// legacyDirectWatchSyncProvider is deliberately implemented without embedding +// UnimplementedWatchSyncProviderServer. Keep this compile-time assertion so a +// future additive RPC cannot silently break source compatibility for v0.12 +// plugins generated with require_unimplemented_servers=false. +type legacyDirectWatchSyncProvider struct{} + +func (legacyDirectWatchSyncProvider) InitAuthorize(context.Context, *WatchSyncInitAuthorizeRequest) (*WatchSyncInitAuthorizeResponse, error) { + return nil, nil +} + +func (legacyDirectWatchSyncProvider) ExchangeCode(context.Context, *WatchSyncExchangeCodeRequest) (*WatchSyncCredentialResponse, error) { + return nil, nil +} + +func (legacyDirectWatchSyncProvider) ExchangeAPIKey(context.Context, *WatchSyncExchangeAPIKeyRequest) (*WatchSyncCredentialResponse, error) { + return nil, nil +} + +func (legacyDirectWatchSyncProvider) RefreshCredentials(context.Context, *WatchSyncRefreshCredentialsRequest) (*WatchSyncCredentialResponse, error) { + return nil, nil +} + +func (legacyDirectWatchSyncProvider) GetAccount(context.Context, *WatchSyncGetAccountRequest) (*WatchSyncGetAccountResponse, error) { + return nil, nil +} + +func (legacyDirectWatchSyncProvider) ApplyEvents(context.Context, *WatchSyncApplyEventsRequest) (*WatchSyncApplyEventsResponse, error) { + return nil, nil +} + +func (legacyDirectWatchSyncProvider) ListRemoteState(context.Context, *WatchSyncListRemoteStateRequest) (*WatchSyncListRemoteStateResponse, error) { + return nil, nil +} + +var _ WatchSyncProviderServer = legacyDirectWatchSyncProvider{} diff --git a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_grpc.pb.go b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_grpc.pb.go index 4257841..c5fbd31 100644 --- a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_grpc.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_grpc.pb.go @@ -355,3 +355,149 @@ var WatchSyncProvider_ServiceDesc = grpc.ServiceDesc{ Streams: []grpc.StreamDesc{}, Metadata: "silo/plugin/v1/watch_sync_provider.proto", } + +const ( + WatchSyncDeviceAuthorizationService_Start_FullMethodName = "/silo.plugin.v1.WatchSyncDeviceAuthorizationService/Start" + WatchSyncDeviceAuthorizationService_Poll_FullMethodName = "/silo.plugin.v1.WatchSyncDeviceAuthorizationService/Poll" +) + +// WatchSyncDeviceAuthorizationServiceClient is the client API for WatchSyncDeviceAuthorizationService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// WatchSyncDeviceAuthorizationService is separate from WatchSyncProvider so adding +// device authorization does not add methods to the released v0.12 Go server +// interface. A plugin advertising DEVICE_CODE registers both services. +type WatchSyncDeviceAuthorizationServiceClient interface { + Start(ctx context.Context, in *WatchSyncDeviceAuthorizationServiceStartRequest, opts ...grpc.CallOption) (*WatchSyncDeviceAuthorizationServiceStartResponse, error) + Poll(ctx context.Context, in *WatchSyncDeviceAuthorizationServicePollRequest, opts ...grpc.CallOption) (*WatchSyncDeviceAuthorizationServicePollResponse, error) +} + +type watchSyncDeviceAuthorizationServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewWatchSyncDeviceAuthorizationServiceClient(cc grpc.ClientConnInterface) WatchSyncDeviceAuthorizationServiceClient { + return &watchSyncDeviceAuthorizationServiceClient{cc} +} + +func (c *watchSyncDeviceAuthorizationServiceClient) Start(ctx context.Context, in *WatchSyncDeviceAuthorizationServiceStartRequest, opts ...grpc.CallOption) (*WatchSyncDeviceAuthorizationServiceStartResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(WatchSyncDeviceAuthorizationServiceStartResponse) + err := c.cc.Invoke(ctx, WatchSyncDeviceAuthorizationService_Start_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *watchSyncDeviceAuthorizationServiceClient) Poll(ctx context.Context, in *WatchSyncDeviceAuthorizationServicePollRequest, opts ...grpc.CallOption) (*WatchSyncDeviceAuthorizationServicePollResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(WatchSyncDeviceAuthorizationServicePollResponse) + err := c.cc.Invoke(ctx, WatchSyncDeviceAuthorizationService_Poll_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// WatchSyncDeviceAuthorizationServiceServer is the server API for WatchSyncDeviceAuthorizationService service. +// All implementations should embed UnimplementedWatchSyncDeviceAuthorizationServiceServer +// for forward compatibility. +// +// WatchSyncDeviceAuthorizationService is separate from WatchSyncProvider so adding +// device authorization does not add methods to the released v0.12 Go server +// interface. A plugin advertising DEVICE_CODE registers both services. +type WatchSyncDeviceAuthorizationServiceServer interface { + Start(context.Context, *WatchSyncDeviceAuthorizationServiceStartRequest) (*WatchSyncDeviceAuthorizationServiceStartResponse, error) + Poll(context.Context, *WatchSyncDeviceAuthorizationServicePollRequest) (*WatchSyncDeviceAuthorizationServicePollResponse, error) +} + +// UnimplementedWatchSyncDeviceAuthorizationServiceServer should be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedWatchSyncDeviceAuthorizationServiceServer struct{} + +func (UnimplementedWatchSyncDeviceAuthorizationServiceServer) Start(context.Context, *WatchSyncDeviceAuthorizationServiceStartRequest) (*WatchSyncDeviceAuthorizationServiceStartResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Start not implemented") +} +func (UnimplementedWatchSyncDeviceAuthorizationServiceServer) Poll(context.Context, *WatchSyncDeviceAuthorizationServicePollRequest) (*WatchSyncDeviceAuthorizationServicePollResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Poll not implemented") +} +func (UnimplementedWatchSyncDeviceAuthorizationServiceServer) testEmbeddedByValue() {} + +// UnsafeWatchSyncDeviceAuthorizationServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to WatchSyncDeviceAuthorizationServiceServer will +// result in compilation errors. +type UnsafeWatchSyncDeviceAuthorizationServiceServer interface { + mustEmbedUnimplementedWatchSyncDeviceAuthorizationServiceServer() +} + +func RegisterWatchSyncDeviceAuthorizationServiceServer(s grpc.ServiceRegistrar, srv WatchSyncDeviceAuthorizationServiceServer) { + // If the following call panics, it indicates UnimplementedWatchSyncDeviceAuthorizationServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&WatchSyncDeviceAuthorizationService_ServiceDesc, srv) +} + +func _WatchSyncDeviceAuthorizationService_Start_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WatchSyncDeviceAuthorizationServiceStartRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WatchSyncDeviceAuthorizationServiceServer).Start(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WatchSyncDeviceAuthorizationService_Start_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WatchSyncDeviceAuthorizationServiceServer).Start(ctx, req.(*WatchSyncDeviceAuthorizationServiceStartRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WatchSyncDeviceAuthorizationService_Poll_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WatchSyncDeviceAuthorizationServicePollRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WatchSyncDeviceAuthorizationServiceServer).Poll(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WatchSyncDeviceAuthorizationService_Poll_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WatchSyncDeviceAuthorizationServiceServer).Poll(ctx, req.(*WatchSyncDeviceAuthorizationServicePollRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// WatchSyncDeviceAuthorizationService_ServiceDesc is the grpc.ServiceDesc for WatchSyncDeviceAuthorizationService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var WatchSyncDeviceAuthorizationService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "silo.plugin.v1.WatchSyncDeviceAuthorizationService", + HandlerType: (*WatchSyncDeviceAuthorizationServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Start", + Handler: _WatchSyncDeviceAuthorizationService_Start_Handler, + }, + { + MethodName: "Poll", + Handler: _WatchSyncDeviceAuthorizationService_Poll_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "silo/plugin/v1/watch_sync_provider.proto", +} diff --git a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go index dff73a3..51e4fba 100644 --- a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go +++ b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go @@ -26,6 +26,7 @@ func TestWatchSyncTypedRemoteStateRoundTrip(t *testing.T) { ProgressPercent: 42.5, PausedAt: timestamppb.New(pausedAt), }, + Watchlist: &WatchSyncRemoteListState{ListedAt: timestamppb.New(pausedAt.Add(-2 * time.Hour)), Removed: true}, }}, NextCursor: "checkpoint-2", CompleteSnapshot: true, @@ -44,11 +45,135 @@ func TestWatchSyncTypedRemoteStateRoundTrip(t *testing.T) { if item.GetMedia().GetMediaType() != WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE || item.GetWatched().GetPlayCount() != 2 || item.GetProgress().GetProgressPercent() != 42.5 || - !item.GetProgress().GetPausedAt().AsTime().Equal(pausedAt) { + !item.GetProgress().GetPausedAt().AsTime().Equal(pausedAt) || + !item.GetWatchlist().GetListedAt().AsTime().Equal(pausedAt.Add(-2*time.Hour)) || + !item.GetWatchlist().GetRemoved() { t.Fatalf("remote state = %#v", item) } } +func TestWatchSyncDeviceAuthorizationRoundTrip(t *testing.T) { + expiresAt := time.Unix(1_800_000_000, 0).UTC() + input := &WatchSyncDeviceAuthorizationServiceStartResponse{ + UserCode: "ABCD-1234", + VerificationUrl: "https://provider.example/activate", + VerificationUrlComplete: "https://provider.example/activate?code=ABCD-1234", + ProviderState: []byte(`{"device_code":"secret"}`), + PollingInterval: durationpb.New(5 * time.Second), + ExpiresAt: timestamppb.New(expiresAt), + } + data, err := proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + var output WatchSyncDeviceAuthorizationServiceStartResponse + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if output.GetUserCode() != input.GetUserCode() || + output.GetVerificationUrl() != input.GetVerificationUrl() || + output.GetVerificationUrlComplete() != input.GetVerificationUrlComplete() || + !proto.Equal(&output, input) || + string(output.GetProviderState()) != string(input.GetProviderState()) || + output.GetPollingInterval().AsDuration() != 5*time.Second || + !output.GetExpiresAt().AsTime().Equal(expiresAt) { + t.Fatalf("device authorization = %#v", &output) + } +} + +func TestWatchSyncDeviceAuthorizationPendingStateRoundTrip(t *testing.T) { + expiresAt := time.Unix(1_800_000_100, 0).UTC() + input := &WatchSyncDeviceAuthorizationServicePollResponse{ + Status: WatchSyncDeviceAuthorizationStatus_WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_PENDING, + ProviderState: []byte("rotated-state"), + PollingInterval: durationpb.New(10 * time.Second), + ExpiresAt: timestamppb.New(expiresAt), + } + data, err := proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + var output WatchSyncDeviceAuthorizationServicePollResponse + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if !proto.Equal(input, &output) { + t.Fatalf("pending device authorization = %#v", &output) + } +} + +func TestWatchSyncDeviceAuthorizationPendingStatePreservesPresence(t *testing.T) { + explicitEmpty := &WatchSyncDeviceAuthorizationServicePollResponse{ProviderState: []byte{}} + data, err := proto.Marshal(explicitEmpty) + if err != nil { + t.Fatal(err) + } + var output WatchSyncDeviceAuthorizationServicePollResponse + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if output.ProviderState == nil || len(output.GetProviderState()) != 0 { + t.Fatalf("explicit empty provider state = %#v", output.ProviderState) + } + + data, err = proto.Marshal(&WatchSyncDeviceAuthorizationServicePollResponse{}) + if err != nil { + t.Fatal(err) + } + output.Reset() + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if output.ProviderState != nil { + t.Fatalf("omitted provider state = %#v, want nil", output.ProviderState) + } +} + +func TestWatchSyncListPositionPreservesPresence(t *testing.T) { + zero := int32(0) + withZero := &WatchSyncEvent{ListPosition: &zero} + data, err := proto.Marshal(withZero) + if err != nil { + t.Fatal(err) + } + var output WatchSyncEvent + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if output.ListPosition == nil || output.GetListPosition() != 0 { + t.Fatalf("explicit zero list position = %#v", output.ListPosition) + } + omittedData, err := proto.Marshal(&WatchSyncEvent{}) + if err != nil { + t.Fatal(err) + } + var omittedOutput WatchSyncEvent + if err := proto.Unmarshal(omittedData, &omittedOutput); err != nil { + t.Fatal(err) + } + if omittedOutput.ListPosition != nil { + t.Fatal("omitted list position unexpectedly has presence") + } +} + +func TestWatchSyncListTombstoneDoesNotRequireMedia(t *testing.T) { + input := &WatchSyncRemoteState{ + ProviderItemKey: "remote-1", + Favorite: &WatchSyncRemoteListState{Removed: true}, + } + data, err := proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + var output WatchSyncRemoteState + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if !proto.Equal(input, &output) || output.GetMedia() != nil || !output.GetFavorite().GetRemoved() { + t.Fatalf("list tombstone = %#v", &output) + } +} + func TestWatchSyncApplyResultCarriesTypedRateLimit(t *testing.T) { retryAfter := 45 * time.Second request := &WatchSyncApplyEventsRequest{ diff --git a/pkg/pluginsdk/convert/watch_sync_provider_test.go b/pkg/pluginsdk/convert/watch_sync_provider_test.go index e554dc4..9c607b9 100644 --- a/pkg/pluginsdk/convert/watch_sync_provider_test.go +++ b/pkg/pluginsdk/convert/watch_sync_provider_test.go @@ -12,8 +12,10 @@ func TestWatchSyncProviderDescriptorRoundTrip(t *testing.T) { Type: "watch_sync_provider.v1", Id: "anilist", WatchSyncProvider: &pluginv1.WatchSyncProviderDescriptor{ - AuthMethods: []pluginv1.WatchSyncAuthMethod{pluginv1.WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_AUTHORIZATION_CODE}, - ExportWatched: true, + AuthMethods: []pluginv1.WatchSyncAuthMethod{pluginv1.WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_DEVICE_CODE}, + ExportWatched: true, + ImportWatchlist: true, + ScrobblePlayback: true, SupportedMediaTypes: []pluginv1.WatchSyncMediaType{ pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE, pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE, @@ -30,8 +32,9 @@ func TestWatchSyncProviderDescriptorRoundTrip(t *testing.T) { t.Fatal(err) } got := decoded.GetWatchSyncProvider() - if got == nil || !got.GetExportWatched() || got.GetMaxBatchSize() != 25 || - len(got.GetAuthMethods()) != 1 || len(got.GetSupportedMediaTypes()) != 2 || + if got == nil || !got.GetExportWatched() || !got.GetImportWatchlist() || !got.GetScrobblePlayback() || got.GetMaxBatchSize() != 25 || + len(got.GetAuthMethods()) != 1 || got.GetAuthMethods()[0] != pluginv1.WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_DEVICE_CODE || + len(got.GetSupportedMediaTypes()) != 2 || got.GetSupportedMediaTypes()[1] != pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE { t.Fatalf("decoded descriptor = %#v", got) } diff --git a/pkg/pluginsdk/manifest/manifest.go b/pkg/pluginsdk/manifest/manifest.go index a5bd670..f6de2c0 100644 --- a/pkg/pluginsdk/manifest/manifest.go +++ b/pkg/pluginsdk/manifest/manifest.go @@ -156,9 +156,17 @@ func validateWatchSyncCapability(descriptor *pluginv1.CapabilityDescriptor) erro return fmt.Errorf("plugin capability %q: watch sync auth method cannot be unspecified", descriptor.GetId()) } } - if !watchSync.GetExportWatched() && !watchSync.GetExportUnwatched() && !watchSync.GetImportWatched() && !watchSync.GetImportProgress() { + if !watchSync.GetExportWatched() && !watchSync.GetExportUnwatched() && + !watchSync.GetImportWatched() && !watchSync.GetImportProgress() && + !watchSync.GetImportFavorites() && !watchSync.GetExportFavorites() && + !watchSync.GetRemoveFavorites() && !watchSync.GetImportWatchlist() && + !watchSync.GetExportWatchlist() && !watchSync.GetRemoveWatchlist() && + !watchSync.GetScrobblePlayback() { return fmt.Errorf("plugin capability %q: at least one watch sync operation is required", descriptor.GetId()) } + if watchSync.GetProvidesWatchlistOrder() && !watchSync.GetImportWatchlist() { + return fmt.Errorf("plugin capability %q: watchlist order requires watchlist import", descriptor.GetId()) + } if watchSync.GetMaxBatchSize() < 1 || watchSync.GetMaxBatchSize() > 100 { return fmt.Errorf("plugin capability %q: watch sync max_batch_size must be between 1 and 100", descriptor.GetId()) } diff --git a/pkg/pluginsdk/manifest/watch_sync_provider_test.go b/pkg/pluginsdk/manifest/watch_sync_provider_test.go index 97f93b7..115b992 100644 --- a/pkg/pluginsdk/manifest/watch_sync_provider_test.go +++ b/pkg/pluginsdk/manifest/watch_sync_provider_test.go @@ -87,6 +87,28 @@ func TestValidateWatchSyncProviderAllowsUnknownFutureEnums(t *testing.T) { } } +func TestValidateWatchSyncProviderAllowsDeviceCodeAndListOnly(t *testing.T) { + manifest := validWatchSyncManifest() + descriptor := manifest.Capabilities[0].WatchSyncProvider + descriptor.AuthMethods = []pluginv1.WatchSyncAuthMethod{ + pluginv1.WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_DEVICE_CODE, + } + descriptor.ExportWatched = false + descriptor.ImportWatchlist = true + descriptor.ProvidesWatchlistOrder = true + if err := publicmanifest.Validate(manifest); err != nil { + t.Fatalf("device-code list provider should be valid: %v", err) + } +} + +func TestValidateWatchSyncProviderRejectsOrderWithoutImport(t *testing.T) { + manifest := validWatchSyncManifest() + manifest.Capabilities[0].WatchSyncProvider.ProvidesWatchlistOrder = true + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected watchlist order without import to fail") + } +} + func validWatchSyncManifest() *pluginv1.PluginManifest { return &pluginv1.PluginManifest{ PluginId: "silo.valid", Version: "1.0.0", diff --git a/pkg/pluginsdk/runtime/capability_servers_compat_test.go b/pkg/pluginsdk/runtime/capability_servers_compat_test.go new file mode 100644 index 0000000..57d193f --- /dev/null +++ b/pkg/pluginsdk/runtime/capability_servers_compat_test.go @@ -0,0 +1,18 @@ +package runtime + +// Compile-time guard for the released v0.12 unkeyed composite literal shape. +// New optional servers belong behind additive registration APIs, not here. +var _ = CapabilityServers{ + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, +} diff --git a/pkg/pluginsdk/runtime/runtime.go b/pkg/pluginsdk/runtime/runtime.go index 4bd1285..09d18b8 100644 --- a/pkg/pluginsdk/runtime/runtime.go +++ b/pkg/pluginsdk/runtime/runtime.go @@ -70,6 +70,24 @@ func DefaultPluginSet(servers CapabilityServers) plugin.PluginSet { } } +// DefaultPluginSetWithWatchSyncDeviceAuthorization returns the default plugin +// set plus the separate device-authorization service. Keeping that server out +// of CapabilityServers preserves the released v0.12 unkeyed struct shape. +func DefaultPluginSetWithWatchSyncDeviceAuthorization( + servers CapabilityServers, + deviceAuthorization pluginv1.WatchSyncDeviceAuthorizationServiceServer, +) plugin.PluginSet { + if deviceAuthorization == nil { + return DefaultPluginSet(servers) + } + return plugin.PluginSet{ + PluginSetName: &grpcPluginWithWatchSyncDeviceAuthorization{ + GRPCPlugin: &GRPCPlugin{Servers: servers}, + deviceAuthorization: deviceAuthorization, + }, + } +} + func NewClient(conn *grpc.ClientConn) *Client { return &Client{conn: conn} } @@ -133,11 +151,31 @@ func (c *Client) WatchSyncProvider() pluginv1.WatchSyncProviderClient { return pluginv1.NewWatchSyncProviderClient(c.conn) } +func (c *Client) WatchSyncDeviceAuthorization() pluginv1.WatchSyncDeviceAuthorizationServiceClient { + return pluginv1.NewWatchSyncDeviceAuthorizationServiceClient(c.conn) +} + type GRPCPlugin struct { plugin.Plugin Servers CapabilityServers } +type grpcPluginWithWatchSyncDeviceAuthorization struct { + *GRPCPlugin + deviceAuthorization pluginv1.WatchSyncDeviceAuthorizationServiceServer +} + +func (p *grpcPluginWithWatchSyncDeviceAuthorization) GRPCServer( + broker *plugin.GRPCBroker, + server *grpc.Server, +) error { + if err := p.GRPCPlugin.GRPCServer(broker, server); err != nil { + return err + } + pluginv1.RegisterWatchSyncDeviceAuthorizationServiceServer(server, p.deviceAuthorization) + return nil +} + func (p *GRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, server *grpc.Server) error { pluginHost.setBroker(broker) if p.Servers.Runtime == nil { diff --git a/pkg/pluginsdk/runtime/serve_manifest.go b/pkg/pluginsdk/runtime/serve_manifest.go index 4d4b6bb..8b7dbdc 100644 --- a/pkg/pluginsdk/runtime/serve_manifest.go +++ b/pkg/pluginsdk/runtime/serve_manifest.go @@ -19,6 +19,24 @@ type manifestRuntime struct { manifest *pluginv1.PluginManifest } +type serveManifestOptions struct { + watchSyncDeviceAuthorization pluginv1.WatchSyncDeviceAuthorizationServiceServer +} + +// ServeManifestOption adds a service without changing the released +// CapabilityServers or ServeConfig struct layouts. +type ServeManifestOption func(*serveManifestOptions) + +// WithWatchSyncDeviceAuthorization registers the separate device-code service +// for a watch-sync provider that advertises DEVICE_CODE authentication. +func WithWatchSyncDeviceAuthorization( + server pluginv1.WatchSyncDeviceAuthorizationServiceServer, +) ServeManifestOption { + return func(options *serveManifestOptions) { + options.watchSyncDeviceAuthorization = server + } +} + func (s *manifestRuntime) GetManifest(context.Context, *pluginv1.GetManifestRequest) (*pluginv1.GetManifestResponse, error) { return &pluginv1.GetManifestResponse{Manifest: s.manifest}, nil } @@ -37,10 +55,33 @@ func (s *manifestRuntime) BindHostBroker(_ context.Context, req *pluginv1.BindHo // servers (the caller supplies only the non-Runtime servers). It never returns; // a fatal manifest error panics, matching a misbuilt plugin's old main(). func ServeManifest(manifestBytes []byte, version string, servers CapabilityServers) { + ServeManifestWithOptions(manifestBytes, version, servers) +} + +// ServeManifestWithOptions is the additive form of ServeManifest for services +// introduced after the released CapabilityServers shape. +func ServeManifestWithOptions( + manifestBytes []byte, + version string, + servers CapabilityServers, + options ...ServeManifestOption, +) { m, err := manifest.LoadWithChecksum(manifestBytes, version) if err != nil { panic(err) } servers.Runtime = &manifestRuntime{manifest: m} - Serve(ServeConfig{Servers: servers}) + var resolved serveManifestOptions + for _, option := range options { + if option != nil { + option(&resolved) + } + } + Serve(ServeConfig{ + Servers: servers, + Plugins: DefaultPluginSetWithWatchSyncDeviceAuthorization( + servers, + resolved.watchSyncDeviceAuthorization, + ), + }) } diff --git a/pkg/pluginsdk/runtime/watch_sync_provider_test.go b/pkg/pluginsdk/runtime/watch_sync_provider_test.go index 4b5d408..d96ba34 100644 --- a/pkg/pluginsdk/runtime/watch_sync_provider_test.go +++ b/pkg/pluginsdk/runtime/watch_sync_provider_test.go @@ -5,6 +5,7 @@ import ( pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" runtime "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/runtime" + "github.com/hashicorp/go-plugin" "google.golang.org/grpc" ) @@ -12,16 +13,27 @@ type stubWatchSyncProvider struct { pluginv1.UnimplementedWatchSyncProviderServer } -func TestGRPCServerRegistersWatchSyncProvider(t *testing.T) { - p := &runtime.GRPCPlugin{Servers: runtime.CapabilityServers{ +type stubWatchSyncDeviceAuthorization struct { + pluginv1.UnimplementedWatchSyncDeviceAuthorizationServiceServer +} + +func TestGRPCServerRegistersWatchSyncServices(t *testing.T) { + plugins := runtime.DefaultPluginSetWithWatchSyncDeviceAuthorization(runtime.CapabilityServers{ Runtime: stubRuntime{}, WatchSyncProvider: stubWatchSyncProvider{}, - }} + }, stubWatchSyncDeviceAuthorization{}) + p, ok := plugins[runtime.PluginSetName].(plugin.GRPCPlugin) + if !ok { + t.Fatalf("watch-sync plugin type = %T, want plugin.GRPCPlugin", plugins[runtime.PluginSetName]) + } srv := grpc.NewServer() if err := p.GRPCServer(nil, srv); err != nil { - t.Fatalf("GRPCServer with WatchSyncProvider = %v, want nil", err) + t.Fatalf("GRPCServer with watch-sync services = %v, want nil", err) } if _, ok := srv.GetServiceInfo()["silo.plugin.v1.WatchSyncProvider"]; !ok { t.Fatalf("WatchSyncProvider service not registered; got %v", srv.GetServiceInfo()) } + if _, ok := srv.GetServiceInfo()["silo.plugin.v1.WatchSyncDeviceAuthorizationService"]; !ok { + t.Fatalf("WatchSyncDeviceAuthorization service not registered; got %v", srv.GetServiceInfo()) + } } diff --git a/proto/silo/plugin/v1/watch_sync_provider.proto b/proto/silo/plugin/v1/watch_sync_provider.proto index eb959d2..c76b717 100644 --- a/proto/silo/plugin/v1/watch_sync_provider.proto +++ b/proto/silo/plugin/v1/watch_sync_provider.proto @@ -22,10 +22,19 @@ service WatchSyncProvider { rpc ListRemoteState(WatchSyncListRemoteStateRequest) returns (WatchSyncListRemoteStateResponse); } +// WatchSyncDeviceAuthorizationService is separate from WatchSyncProvider so adding +// device authorization does not add methods to the released v0.12 Go server +// interface. A plugin advertising DEVICE_CODE registers both services. +service WatchSyncDeviceAuthorizationService { + rpc Start(WatchSyncDeviceAuthorizationServiceStartRequest) returns (WatchSyncDeviceAuthorizationServiceStartResponse); + rpc Poll(WatchSyncDeviceAuthorizationServicePollRequest) returns (WatchSyncDeviceAuthorizationServicePollResponse); +} + enum WatchSyncAuthMethod { WATCH_SYNC_AUTH_METHOD_UNSPECIFIED = 0; WATCH_SYNC_AUTH_METHOD_AUTHORIZATION_CODE = 1; WATCH_SYNC_AUTH_METHOD_API_KEY = 2; + WATCH_SYNC_AUTH_METHOD_DEVICE_CODE = 3; } enum WatchSyncMediaType { @@ -38,6 +47,29 @@ enum WatchSyncOperation { WATCH_SYNC_OPERATION_UNSPECIFIED = 0; WATCH_SYNC_OPERATION_MARK_WATCHED = 1; WATCH_SYNC_OPERATION_MARK_UNWATCHED = 2; + WATCH_SYNC_OPERATION_ADD_FAVORITE = 3; + WATCH_SYNC_OPERATION_REMOVE_FAVORITE = 4; + WATCH_SYNC_OPERATION_ADD_TO_WATCHLIST = 5; + WATCH_SYNC_OPERATION_REMOVE_FROM_WATCHLIST = 6; + WATCH_SYNC_OPERATION_SCROBBLE_START = 7; + WATCH_SYNC_OPERATION_SCROBBLE_PAUSE = 8; + WATCH_SYNC_OPERATION_SCROBBLE_STOP = 9; +} + +enum WatchSyncRemoteStateKind { + WATCH_SYNC_REMOTE_STATE_KIND_UNSPECIFIED = 0; + WATCH_SYNC_REMOTE_STATE_KIND_WATCHED = 1; + WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS = 2; + WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE = 3; + WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST = 4; +} + +enum WatchSyncDeviceAuthorizationStatus { + WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_UNSPECIFIED = 0; + WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_PENDING = 1; + WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_AUTHORIZED = 2; + WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_DENIED = 3; + WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_EXPIRED = 4; } enum WatchSyncOrigin { @@ -74,6 +106,17 @@ message WatchSyncProviderDescriptor { repeated WatchSyncMediaType supported_media_types = 6; repeated string external_id_namespaces = 7; int32 max_batch_size = 8; + bool import_favorites = 9; + bool export_favorites = 10; + bool remove_favorites = 11; + bool import_watchlist = 12; + bool export_watchlist = 13; + bool remove_watchlist = 14; + // The order of WATCHLIST states returned by ListRemoteState is meaningful. + // WATCHLIST traversals must set complete_snapshot=true when this is enabled; + // an incremental subset cannot define positions relative to omitted items. + bool provides_watchlist_order = 15; + bool scrobble_playback = 16; } message WatchSyncProviderConfig { @@ -157,6 +200,54 @@ message WatchSyncExchangeAPIKeyRequest { string api_key = 3; } +message WatchSyncDeviceAuthorizationServiceStartRequest { + string capability_id = 1; + WatchSyncProviderConfig provider_config = 2; +} + +message WatchSyncDeviceAuthorizationServiceStartResponse { + string user_code = 1; + string verification_url = 2; + string verification_url_complete = 3; + // Plugin-defined flow data. The host encrypts it until authorization + // completes and never returns it through the public HTTP API. + bytes provider_state = 4; + google.protobuf.Duration polling_interval = 5; + google.protobuf.Timestamp expires_at = 6; + WatchSyncFault fault = 7; +} + +message WatchSyncDeviceAuthorizationServicePollRequest { + string capability_id = 1; + WatchSyncProviderConfig provider_config = 2; + bytes provider_state = 3; +} + +message WatchSyncDeviceAuthorizationServicePollResponse { + WatchSyncDeviceAuthorizationStatus status = 1; + // Complete authoritative replacement when status is AUTHORIZED. The host + // persists it before consuming account or fault. + WatchSyncCredentials credentials = 2; + WatchSyncAccount account = 3; + // PENDING normally omits fault. DENIED and EXPIRED may use a safe, + // non-retryable fault. Connection-wide and transport failures use fault. + WatchSyncFault fault = 4; + // A PENDING response may update host-private state and timing only for the + // same user challenge. The user_code and verification URLs returned by Start + // must remain valid through expires_at; if those instructions change, expire + // this flow and require a new Start call. + // + // Optional authoritative replacement for provider_state while PENDING. The + // host encrypts and uses it for the next poll. Presence is significant: an + // explicitly empty value clears the prior state, while omission retains it. + optional bytes provider_state = 5; + // Optional replacement polling interval while PENDING, for example after an + // OAuth device-flow slow_down response. + google.protobuf.Duration polling_interval = 6; + // Optional replacement expiry for the same user challenge. + google.protobuf.Timestamp expires_at = 7; +} + message WatchSyncRefreshCredentialsRequest { WatchSyncAuthenticatedContext context = 1; } @@ -211,6 +302,13 @@ message WatchSyncEvent { double duration_seconds = 9; double completion_percent = 10; int32 play_count = 11; + // Optional zero-based position for list providers that accept explicit + // ordering. Providers may ignore it unless their API supports order writes. + optional int32 list_position = 12; + // Provider-native key previously returned by ListRemoteState. It lets a + // removal address the exact upstream record when external IDs are absent or + // no longer resolve. + string provider_item_key = 13; } message WatchSyncApplyEventsRequest { @@ -252,6 +350,9 @@ message WatchSyncListRemoteStateRequest { string page_token = 3; // Requested maximum number of items. Plugins may return fewer. int32 page_size = 4; + // State families requested by the host. Empty means every family advertised + // by the descriptor, preserving compatibility with v0.12 hosts and plugins. + repeated WatchSyncRemoteStateKind state_kinds = 5; } message WatchSyncRemoteWatchedState { @@ -269,13 +370,25 @@ message WatchSyncRemoteProgressState { google.protobuf.Timestamp paused_at = 2; } +message WatchSyncRemoteListState { + // Provider timestamp for when the item was added, when available. + google.protobuf.Timestamp listed_at = 1; + // True is an explicit tombstone. It is meaningful for incremental + // traversals, where omitting an item is not a deletion. + bool removed = 2; +} + message WatchSyncRemoteState { string provider_item_key = 1; + // Required except on an explicit favorite/watchlist tombstone whose + // provider_item_key identifies a record returned previously. WatchSyncMedia media = 2; // At least one typed state must be present. Both may be present when the // provider reports a completed play and a separate resume point. WatchSyncRemoteWatchedState watched = 3; WatchSyncRemoteProgressState progress = 4; + WatchSyncRemoteListState favorite = 5; + WatchSyncRemoteListState watchlist = 6; } message WatchSyncListRemoteStateResponse { @@ -288,7 +401,9 @@ message WatchSyncListRemoteStateResponse { string next_cursor = 3; // True when this traversal is an authoritative full snapshot. False means an // incremental delta, so an absent item must not be interpreted as deletion. - // The value must remain stable across every page in one traversal. + // The value must remain stable across every page in one traversal. A provider + // advertising provides_watchlist_order must set this true for WATCHLIST + // traversals so the returned order is unambiguous. bool complete_snapshot = 4; // Complete authoritative replacement. The host persists it before consuming // this page or fault; a persistence failure discards the page and cursor. From 214cfe5ca131462a6d33245825f5a224b43cbc2d Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:41:05 -0400 Subject: [PATCH 02/17] fix(convert): preserve capability config forms (#14) * fix(convert): preserve capability config forms * fix(convert): preserve config metadata defaults --- pkg/pluginsdk/convert/convert.go | 53 +++++++++++++-------------- pkg/pluginsdk/convert/convert_test.go | 27 +++++++++++++- 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/pkg/pluginsdk/convert/convert.go b/pkg/pluginsdk/convert/convert.go index a05658a..65fa2f0 100644 --- a/pkg/pluginsdk/convert/convert.go +++ b/pkg/pluginsdk/convert/convert.go @@ -125,13 +125,28 @@ func capabilityMetadata(descriptor *pluginv1.CapabilityDescriptor) (map[string]a if len(descriptor.GetConfigSchema()) > 0 { schemas := make([]map[string]any, 0, len(descriptor.GetConfigSchema())) for _, schema := range descriptor.GetConfigSchema() { - schemas = append(schemas, map[string]any{ - "key": schema.GetKey(), - "title": schema.GetTitle(), - "description": schema.GetDescription(), - "json_schema": schema.GetJsonSchema(), - "required": schema.GetRequired(), - }) + if schema == nil { + schemas = append(schemas, map[string]any{}) + continue + } + data, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(schema) + if err != nil { + return nil, fmt.Errorf("encode capability config schema: %w", err) + } + var value map[string]any + if err := json.Unmarshal(data, &value); err != nil { + return nil, fmt.Errorf("decode capability config schema JSON: %w", err) + } + // CapabilityRecord.Metadata is a public compatibility boundary. The + // original scalar-only encoding always emitted these keys, including + // required=false; protojson omits default-valued fields unless they are + // restored explicitly. + value["key"] = schema.GetKey() + value["title"] = schema.GetTitle() + value["description"] = schema.GetDescription() + value["json_schema"] = schema.GetJsonSchema() + value["required"] = schema.GetRequired() + schemas = append(schemas, value) } metadata["config_schema"] = schemas } @@ -157,19 +172,13 @@ func decodeConfigSchemas(value any) ([]*pluginv1.ConfigSchema, error) { schemas := make([]*pluginv1.ConfigSchema, 0, len(entries)) for _, entry := range entries { rawSchema := toStringAnyMap(entry) - jsonData, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(&pluginv1.ConfigSchema{ - Key: stringValue(rawSchema["key"]), - Title: stringValue(rawSchema["title"]), - Description: stringValue(rawSchema["description"]), - JsonSchema: stringValue(rawSchema["json_schema"]), - Required: boolValue(rawSchema["required"]), - }) + jsonData, err := json.Marshal(rawSchema) if err != nil { - return nil, fmt.Errorf("encode config schema: %w", err) + return nil, fmt.Errorf("encode capability config schema: %w", err) } var schema pluginv1.ConfigSchema - if err := protojson.Unmarshal(jsonData, &schema); err != nil { - return nil, fmt.Errorf("decode config schema: %w", err) + if err := (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(jsonData, &schema); err != nil { + return nil, fmt.Errorf("decode capability config schema: %w", err) } schemas = append(schemas, &schema) } @@ -206,13 +215,3 @@ func toStringAnyMap(value any) map[string]any { return map[string]any{} } } - -func stringValue(value any) string { - text, _ := value.(string) - return text -} - -func boolValue(value any) bool { - flag, _ := value.(bool) - return flag -} diff --git a/pkg/pluginsdk/convert/convert_test.go b/pkg/pluginsdk/convert/convert_test.go index 762283f..b1719f4 100644 --- a/pkg/pluginsdk/convert/convert_test.go +++ b/pkg/pluginsdk/convert/convert_test.go @@ -3,6 +3,7 @@ package convert_test import ( "testing" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" @@ -34,7 +35,15 @@ func TestCapabilityRecordsFromManifestRoundTrips(t *testing.T) { Title: "Connection", Description: "API key", JsonSchema: `{"type":"object"}`, - Required: true, + Required: false, + AdminForm: &pluginv1.AdminFormDescriptor{ + SubmitLabel: "Connect", + Fields: []*pluginv1.AdminFormField{{ + Key: "base_url", Label: "Server URL", + Control: pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_TEXT, + Required: true, + }}, + }, }, }, Metadata: metadata, @@ -49,6 +58,19 @@ func TestCapabilityRecordsFromManifestRoundTrips(t *testing.T) { if len(records) != 1 { t.Fatalf("record count = %d, want 1", len(records)) } + recordedSchema := records[0].Metadata["config_schema"].([]map[string]any)[0] + for key, want := range map[string]any{ + "key": "connection", + "title": "Connection", + "description": "API key", + "json_schema": `{"type":"object"}`, + "required": false, + } { + if got, present := recordedSchema[key]; !present || got != want { + t.Fatalf("config_schema[%q] = %#v, present=%v, want %#v", key, got, present, want) + } + } + recordedSchema["future_field"] = true decoded, err := convert.DecodeCapability(records[0]) if err != nil { @@ -60,6 +82,9 @@ func TestCapabilityRecordsFromManifestRoundTrips(t *testing.T) { if got := decoded.GetConfigSchema()[0].GetKey(); got != "connection" { t.Fatalf("config_schema key = %q, want connection", got) } + if !proto.Equal(decoded.GetConfigSchema()[0], manifest.GetCapabilities()[0].GetConfigSchema()[0]) { + t.Fatalf("config_schema round trip = %#v, want %#v", decoded.GetConfigSchema()[0], manifest.GetCapabilities()[0].GetConfigSchema()[0]) + } if got := decoded.GetMetadata().AsMap()["provider"]; got != "example" { t.Fatalf("metadata provider = %#v, want example", got) } From 1ad0fe54408e99d35e6aee86c489a0edd528f6b2 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:58:49 -0400 Subject: [PATCH 03/17] feat(watchsync): expose authoritative completion state (#15) --- README.md | 2 ++ .../silo/plugin/v1/watch_sync_provider.pb.go | 20 +++++++++--- .../plugin/v1/watch_sync_provider_test.go | 32 +++++++++++++++++++ .../silo/plugin/v1/watch_sync_provider.proto | 4 +++ 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 71d8db5..b1fe23f 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,8 @@ provider flow state, or secret configuration. `ApplyEvents` is an at-least-once contract; plugins must treat `event_id` as stable across retries and implement convergent desired-state updates rather than increments. That rule also applies to scrobble stops: replaying the same event ID must not create another play. +For playback events, `completed` is the host's authoritative watched decision; +plugins must not infer completion from `watch_history_id` or percentage alone. Authenticated RPCs receive the same host-owned capability, configuration, and credential data through `WatchSyncAuthenticatedContext`. The context exists diff --git a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider.pb.go b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider.pb.go index 91bfbd2..d561e27 100644 --- a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider.pb.go @@ -1950,8 +1950,12 @@ type WatchSyncEvent struct { // removal address the exact upstream record when external IDs are absent or // no longer resolve. ProviderItemKey string `protobuf:"bytes,13,opt,name=provider_item_key,json=providerItemKey,proto3" json:"provider_item_key,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Authoritative host completion state for playback events. A history ID can + // exist for incomplete playback, and completion_percent may be below 100 + // when the host's configured watched threshold has been reached. + Completed bool `protobuf:"varint,14,opt,name=completed,proto3" json:"completed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *WatchSyncEvent) Reset() { @@ -2075,6 +2079,13 @@ func (x *WatchSyncEvent) GetProviderItemKey() string { return "" } +func (x *WatchSyncEvent) GetCompleted() bool { + if x != nil { + return x.Completed + } + return false +} + type WatchSyncApplyEventsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Context *WatchSyncAuthenticatedContext `protobuf:"bytes,1,opt,name=context,proto3" json:"context,omitempty"` @@ -2833,7 +2844,7 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aD\n" + "\x16SeriesExternalIdsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xff\x04\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x9d\x05\n" + "\x0eWatchSyncEvent\x12\x19\n" + "\bevent_id\x18\x01 \x01(\tR\aeventId\x12@\n" + "\toperation\x18\x02 \x01(\x0e2\".silo.plugin.v1.WatchSyncOperationR\toperation\x127\n" + @@ -2850,7 +2861,8 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\n" + "play_count\x18\v \x01(\x05R\tplayCount\x12(\n" + "\rlist_position\x18\f \x01(\x05H\x00R\flistPosition\x88\x01\x01\x12*\n" + - "\x11provider_item_key\x18\r \x01(\tR\x0fproviderItemKeyB\x10\n" + + "\x11provider_item_key\x18\r \x01(\tR\x0fproviderItemKey\x12\x1c\n" + + "\tcompleted\x18\x0e \x01(\bR\tcompletedB\x10\n" + "\x0e_list_position\"\x9e\x01\n" + "\x1bWatchSyncApplyEventsRequest\x12G\n" + "\acontext\x18\x01 \x01(\v2-.silo.plugin.v1.WatchSyncAuthenticatedContextR\acontext\x126\n" + diff --git a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go index 51e4fba..4e21030 100644 --- a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go +++ b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go @@ -212,3 +212,35 @@ func TestWatchSyncApplyResultCarriesTypedRateLimit(t *testing.T) { t.Fatalf("request=%#v result=%#v", request, &output) } } + +func TestWatchSyncEventCarriesAuthoritativeCompletionState(t *testing.T) { + input := &WatchSyncEvent{ + EventId: "playback-stop-1", + WatchHistoryId: "incomplete-history-row", + CompletionPercent: 5.5, + Completed: false, + } + data, err := proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + var output WatchSyncEvent + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if output.GetCompleted() { + t.Fatal("completed = true, want false") + } + + input.Completed = true + data, err = proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if !output.GetCompleted() { + t.Fatal("completed = false, want true") + } +} diff --git a/proto/silo/plugin/v1/watch_sync_provider.proto b/proto/silo/plugin/v1/watch_sync_provider.proto index c76b717..b8b0033 100644 --- a/proto/silo/plugin/v1/watch_sync_provider.proto +++ b/proto/silo/plugin/v1/watch_sync_provider.proto @@ -309,6 +309,10 @@ message WatchSyncEvent { // removal address the exact upstream record when external IDs are absent or // no longer resolve. string provider_item_key = 13; + // Authoritative host completion state for playback events. A history ID can + // exist for incomplete playback, and completion_percent may be below 100 + // when the host's configured watched threshold has been reached. + bool completed = 14; } message WatchSyncApplyEventsRequest { From 636acff04b36fd4fb414eb2bfdcdb04fab3dd621 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:37:00 -0400 Subject: [PATCH 04/17] build(deps): bump google.golang.org/grpc from 1.75.1 to 1.82.1 (#12) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.75.1 to 1.82.1. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.75.1...v1.82.1) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.82.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 10 +++++----- go.sum | 50 ++++++++++++++++++++++++++------------------------ 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index a26e4bb..64e39e2 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.7.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 - google.golang.org/grpc v1.75.1 + google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 ) @@ -17,8 +17,8 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/oklog/run v1.1.0 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect ) diff --git a/go.sum b/go.sum index f0c0a85..6681331 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -41,36 +43,36 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCw github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= -google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 0950ea3249991437cead0d5b42fe059c3ea531d9 Mon Sep 17 00:00:00 2001 From: blurbery <41898282+blurbery@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:37:13 +1000 Subject: [PATCH 05/17] feat(metadata): support scoped season image galleries --- .../silo/plugin/v1/metadata_provider.pb.go | 56 ++++++++++++++----- .../silo/plugin/v1/metadata_provider_test.go | 47 ++++++++++++++++ proto/silo/plugin/v1/metadata_provider.proto | 6 ++ 3 files changed, 94 insertions(+), 15 deletions(-) diff --git a/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go b/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go index 1d0b224..31cd31d 100644 --- a/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go @@ -1680,13 +1680,15 @@ func (x *GetEpisodesResponse) GetEpisodes() []*EpisodeRecord { } type ImageRecord struct { - state protoimpl.MessageState `protogen:"open.v1"` - Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` - Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` - Language string `protobuf:"bytes,3,opt,name=language,proto3" json:"language,omitempty"` - Width int32 `protobuf:"varint,4,opt,name=width,proto3" json:"width,omitempty"` - Height int32 `protobuf:"varint,5,opt,name=height,proto3" json:"height,omitempty"` - Metadata *structpb.Struct `protobuf:"bytes,6,opt,name=metadata,proto3" json:"metadata,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` + Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` + Language string `protobuf:"bytes,3,opt,name=language,proto3" json:"language,omitempty"` + Width int32 `protobuf:"varint,4,opt,name=width,proto3" json:"width,omitempty"` + Height int32 `protobuf:"varint,5,opt,name=height,proto3" json:"height,omitempty"` + Metadata *structpb.Struct `protobuf:"bytes,6,opt,name=metadata,proto3" json:"metadata,omitempty"` + // Present only when the image is confirmed to belong to one exact season. + SeasonNumber *int32 `protobuf:"varint,7,opt,name=season_number,json=seasonNumber,proto3,oneof" json:"season_number,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1763,12 +1765,23 @@ func (x *ImageRecord) GetMetadata() *structpb.Struct { return nil } +func (x *ImageRecord) GetSeasonNumber() int32 { + if x != nil && x.SeasonNumber != nil { + return *x.SeasonNumber + } + return 0 +} + type GetImagesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ProviderId string `protobuf:"bytes,1,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` - ItemType string `protobuf:"bytes,2,opt,name=item_type,json=itemType,proto3" json:"item_type,omitempty"` - ProviderIds *structpb.Struct `protobuf:"bytes,3,opt,name=provider_ids,json=providerIds,proto3" json:"provider_ids,omitempty"` - Language string `protobuf:"bytes,4,opt,name=language,proto3" json:"language,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + ProviderId string `protobuf:"bytes,1,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` + ItemType string `protobuf:"bytes,2,opt,name=item_type,json=itemType,proto3" json:"item_type,omitempty"` + ProviderIds *structpb.Struct `protobuf:"bytes,3,opt,name=provider_ids,json=providerIds,proto3" json:"provider_ids,omitempty"` + Language string `protobuf:"bytes,4,opt,name=language,proto3" json:"language,omitempty"` + // Present only when requesting artwork for a specific TV season. + // Season zero represents Specials, so presence must not be inferred from + // the numeric value. + SeasonNumber *int32 `protobuf:"varint,5,opt,name=season_number,json=seasonNumber,proto3,oneof" json:"season_number,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1831,6 +1844,13 @@ func (x *GetImagesRequest) GetLanguage() string { return "" } +func (x *GetImagesRequest) GetSeasonNumber() int32 { + if x != nil && x.SeasonNumber != nil { + return *x.SeasonNumber + } + return 0 +} + type GetImagesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Images []*ImageRecord `protobuf:"bytes,1,rep,name=images,proto3" json:"images,omitempty"` @@ -2236,20 +2256,24 @@ const file_silo_plugin_v1_metadata_provider_proto_rawDesc = "" + "\fprovider_ids\x18\x03 \x01(\v2\x17.google.protobuf.StructR\vproviderIds\x12\x1a\n" + "\blanguage\x18\x04 \x01(\tR\blanguage\"P\n" + "\x13GetEpisodesResponse\x129\n" + - "\bepisodes\x18\x01 \x03(\v2\x1d.silo.plugin.v1.EpisodeRecordR\bepisodes\"\xb2\x01\n" + + "\bepisodes\x18\x01 \x03(\v2\x1d.silo.plugin.v1.EpisodeRecordR\bepisodes\"\xee\x01\n" + "\vImageRecord\x12\x12\n" + "\x04kind\x18\x01 \x01(\tR\x04kind\x12\x10\n" + "\x03url\x18\x02 \x01(\tR\x03url\x12\x1a\n" + "\blanguage\x18\x03 \x01(\tR\blanguage\x12\x14\n" + "\x05width\x18\x04 \x01(\x05R\x05width\x12\x16\n" + "\x06height\x18\x05 \x01(\x05R\x06height\x123\n" + - "\bmetadata\x18\x06 \x01(\v2\x17.google.protobuf.StructR\bmetadata\"\xa8\x01\n" + + "\bmetadata\x18\x06 \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12(\n" + + "\rseason_number\x18\a \x01(\x05H\x00R\fseasonNumber\x88\x01\x01B\x10\n" + + "\x0e_season_number\"\xe4\x01\n" + "\x10GetImagesRequest\x12\x1f\n" + "\vprovider_id\x18\x01 \x01(\tR\n" + "providerId\x12\x1b\n" + "\titem_type\x18\x02 \x01(\tR\bitemType\x12:\n" + "\fprovider_ids\x18\x03 \x01(\v2\x17.google.protobuf.StructR\vproviderIds\x12\x1a\n" + - "\blanguage\x18\x04 \x01(\tR\blanguage\"H\n" + + "\blanguage\x18\x04 \x01(\tR\blanguage\x12(\n" + + "\rseason_number\x18\x05 \x01(\x05H\x00R\fseasonNumber\x88\x01\x01B\x10\n" + + "\x0e_season_number\"H\n" + "\x11GetImagesResponse\x123\n" + "\x06images\x18\x01 \x03(\v2\x1b.silo.plugin.v1.ImageRecordR\x06images\"F\n" + "\x16ResolveImageURLRequest\x12\x12\n" + @@ -2383,6 +2407,8 @@ func file_silo_plugin_v1_metadata_provider_proto_init() { if File_silo_plugin_v1_metadata_provider_proto != nil { return } + file_silo_plugin_v1_metadata_provider_proto_msgTypes[18].OneofWrappers = []any{} + file_silo_plugin_v1_metadata_provider_proto_msgTypes[19].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/pkg/pluginproto/silo/plugin/v1/metadata_provider_test.go b/pkg/pluginproto/silo/plugin/v1/metadata_provider_test.go index ea803db..c6fb3a3 100644 --- a/pkg/pluginproto/silo/plugin/v1/metadata_provider_test.go +++ b/pkg/pluginproto/silo/plugin/v1/metadata_provider_test.go @@ -123,6 +123,11 @@ func TestMetadataProviderRequestDescriptors_IncludeProviderContext(t *testing.T) fieldName: "language", message: &GetImagesRequest{}, }, + { + name: "GetImagesRequest season_number", + fieldName: "season_number", + message: &GetImagesRequest{}, + }, { name: "SearchMetadataRequest language", fieldName: "language", @@ -149,6 +154,48 @@ func TestMetadataProviderRequestDescriptors_IncludeProviderContext(t *testing.T) } } +func TestGetImagesRequest_SeasonNumberPreservesPresenceAndSpecials(t *testing.T) { + seasonZero := int32(0) + payload, err := proto.Marshal(&GetImagesRequest{SeasonNumber: &seasonZero}) + if err != nil { + t.Fatalf("marshal request: %v", err) + } + + var decoded GetImagesRequest + if err := proto.Unmarshal(payload, &decoded); err != nil { + t.Fatalf("unmarshal request: %v", err) + } + if decoded.SeasonNumber == nil { + t.Fatal("season_number presence was lost for Specials") + } + if decoded.GetSeasonNumber() != 0 { + t.Fatalf("season_number = %d, want 0", decoded.GetSeasonNumber()) + } + if (&GetImagesRequest{}).SeasonNumber != nil { + t.Fatal("season_number should be absent for item-level image requests") + } +} + +func TestImageRecord_SeasonNumberPreservesPresenceAndSpecials(t *testing.T) { + seasonZero := int32(0) + payload, err := proto.Marshal(&ImageRecord{SeasonNumber: &seasonZero}) + if err != nil { + t.Fatalf("marshal image: %v", err) + } + + var decoded ImageRecord + if err := proto.Unmarshal(payload, &decoded); err != nil { + t.Fatalf("unmarshal image: %v", err) + } + if decoded.SeasonNumber == nil || decoded.GetSeasonNumber() != 0 { + t.Fatalf("season_number = %v, want present Specials value 0", decoded.SeasonNumber) + } + field := decoded.ProtoReflect().Descriptor().Fields().ByName("season_number") + if field == nil || field.Number() != 7 { + t.Fatalf("season_number descriptor = %v, want field 7", field) + } +} + func TestMetadataProviderServiceDescriptor_IncludesPersonDetailRPC(t *testing.T) { method := File_silo_plugin_v1_metadata_provider_proto.Services(). ByName("MetadataProvider"). diff --git a/proto/silo/plugin/v1/metadata_provider.proto b/proto/silo/plugin/v1/metadata_provider.proto index 1df8018..8e9f582 100644 --- a/proto/silo/plugin/v1/metadata_provider.proto +++ b/proto/silo/plugin/v1/metadata_provider.proto @@ -229,6 +229,8 @@ message ImageRecord { int32 width = 4; int32 height = 5; google.protobuf.Struct metadata = 6; + // Present only when the image is confirmed to belong to one exact season. + optional int32 season_number = 7; } message GetImagesRequest { @@ -236,6 +238,10 @@ message GetImagesRequest { string item_type = 2; google.protobuf.Struct provider_ids = 3; string language = 4; + // Present only when requesting artwork for a specific TV season. + // Season zero represents Specials, so presence must not be inferred from + // the numeric value. + optional int32 season_number = 5; } message GetImagesResponse { From f8a46f6069cd86d47aee913843e6d209f4e72b07 Mon Sep 17 00:00:00 2001 From: Quick104 <31828688+Quick104@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:45:18 -0400 Subject: [PATCH 06/17] feat(imagevariant): add the "large" image-variant tier Silo sends metadata and image-resolver plugins a semantic image-variant hint on ResolveImageURL(s)Request and ResolveCatalogImageURLsRequest, but the vocabulary was documented nowhere and plugins hardcoded the strings. The server now supports client-selectable image sizes and will start sending "large" (~780px posters/stills, ~1280px logos/backdrops) between "featured" and "full". Export the canonical values from a new pkg/pluginsdk/imagevariant package, document the vocabulary on the proto fields and in docs, and state the two contract rules: the set is open and grows additively, and a plugin receiving an unknown variant must degrade to its nearest supported size rather than error. Additive and comment-only on the wire: the regenerated .pb.go diff is struct-field doc comments only. Co-Authored-By: Claude Fable 5 --- docs/compatibility.md | 23 +++++++++ docs/runtime-host.md | 26 +++++++++- .../silo/plugin/v1/metadata_provider.pb.go | 24 ++++++--- .../silo/plugin/v1/runtime_host.pb.go | 10 ++-- pkg/pluginsdk/imagevariant/imagevariant.go | 50 +++++++++++++++++++ .../imagevariant/imagevariant_test.go | 21 ++++++++ proto/silo/plugin/v1/metadata_provider.proto | 12 +++++ proto/silo/plugin/v1/runtime_host.proto | 10 ++-- 8 files changed, 163 insertions(+), 13 deletions(-) create mode 100644 pkg/pluginsdk/imagevariant/imagevariant.go create mode 100644 pkg/pluginsdk/imagevariant/imagevariant_test.go diff --git a/docs/compatibility.md b/docs/compatibility.md index a2502d4..59068fe 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -13,6 +13,7 @@ The compatibility boundary includes: - manifest helpers in `pkg/pluginsdk/manifest` - config validation helpers in `pkg/pluginsdk/config` - generic capability metadata conversion helpers in `pkg/pluginsdk/convert` +- canonical image-variant strings in `pkg/pluginsdk/imagevariant` ## Versioning Rules @@ -29,6 +30,28 @@ The compatibility boundary includes: - CI and release pipelines should build with `GOWORK=off` and without checking out this repo as a sibling source dependency. - Local `go.work` files and temporary `replace` directives are acceptable for development, but they must not be committed as the release path. +## Open Vocabularies + +Some contract fields carry an open string vocabulary rather than an enum, so +Silo can add values without a breaking protobuf change. Plugins must tolerate +values they do not recognize. + +Image variants (`ResolveImageURLRequest.variant`, +`ResolveImageURLsRequest.variant`, `ResolveCatalogImageURLsRequest.variant`) are +the current example. The canonical values are exported from +`pkg/pluginsdk/imagevariant`: `card`, `featured`, `large`, `full`, `original`, +listed smallest to largest. `large` (~780px posters and stills, ~1280px logos +and backdrops) was added between `featured` and `full` once Silo gained +client-selectable image sizes; adding it is an additive change and does not +require a plugin update. + +A plugin receiving an unknown variant MUST degrade gracefully to its nearest +supported size — or its default size — and MUST NOT return an error. Returning +an error turns a slightly-wrong image size into a missing image. Use a `switch` +with a `default` arm rather than an exhaustive match, and do not assume the +constants shipped in any given SDK tag are the complete set. The same rule +applies to any future open vocabulary added to `v1`. + ## Runtime Compatibility - `silo_api_version` is the coarse runtime compatibility gate between Silo and a plugin binary. diff --git a/docs/runtime-host.md b/docs/runtime-host.md index bdf4dc4..ef040a9 100644 --- a/docs/runtime-host.md +++ b/docs/runtime-host.md @@ -109,7 +109,9 @@ Catalog image fields are host-owned opaque paths. Pass poster or backdrop paths back unchanged to resolve them into browser targets: ```go -urls, err := host.ResolveCatalogImageURLs(ctx, []string{item.PosterURL, item.BackdropURL}, "thumbnail") +import "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/imagevariant" + +urls, err := host.ResolveCatalogImageURLs(ctx, []string{item.PosterURL, item.BackdropURL}, imagevariant.Card) if err != nil { return } posterURL := urls[item.PosterURL] ``` @@ -118,6 +120,28 @@ Pass an empty variant to use the host default. Unresolved paths are omitted from the returned map, which is keyed by the original input path. Treat returned URLs as opaque host-generated browser targets. +### Image variants + +The variant is a semantic size hint, not an exact pixel contract. The canonical +vocabulary, smallest to largest, is exported as constants from +`pkg/pluginsdk/imagevariant`: + +| Constant | Value | Intended surface | +| --- | --- | --- | +| `imagevariant.Card` | `card` | grid and list thumbnails, search results, cast rows (~300px posters) | +| `imagevariant.Featured` | `featured` | hero artwork, detail pages, section headers (~500px posters/logos, ~1280px backdrops) | +| `imagevariant.Large` | `large` | high-density displays and client-selected larger sizes (~780px posters/stills, ~1280px logos/backdrops) | +| `imagevariant.Full` | `full` | full-screen and near-source rendering | +| `imagevariant.Original` | `original` | unresized source asset | + +`large` was added after the initial vocabulary; the set is open and may grow +again. A plugin that resolves variants itself (for example a metadata provider +implementing `ResolveImageURL` / `ResolveImageURLs`) must treat an unknown +variant as a request for its nearest supported size — or its default — and must +never return an error. Implement the mapping as a `switch` with a `default` arm +so new variants fall through to a sensible size instead of failing a resolve, +which users see as a missing poster. + ### Peer discovery and calls Use capability constants and discovery helpers instead of hard-coded strings diff --git a/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go b/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go index 1d0b224..594b796 100644 --- a/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go @@ -1876,9 +1876,15 @@ func (x *GetImagesResponse) GetImages() []*ImageRecord { } type ResolveImageURLRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Variant string `protobuf:"bytes,2,opt,name=variant,proto3" json:"variant,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + // Semantic size hint, not an exact pixel contract. Canonical values, smallest + // to largest: "card", "featured", "large", "full", "original". Empty means + // the plugin default. The vocabulary is open and grows additively, so a + // plugin receiving an unknown variant must degrade gracefully to its nearest + // supported size and must not return an error. See + // pkg/pluginsdk/imagevariant for constants and the full rules. + Variant string `protobuf:"bytes,2,opt,name=variant,proto3" json:"variant,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1972,9 +1978,15 @@ func (x *ResolveImageURLResponse) GetUrl() string { } type ResolveImageURLsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Paths []string `protobuf:"bytes,1,rep,name=paths,proto3" json:"paths,omitempty"` - Variant string `protobuf:"bytes,2,opt,name=variant,proto3" json:"variant,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Paths []string `protobuf:"bytes,1,rep,name=paths,proto3" json:"paths,omitempty"` + // Semantic size hint applied to every path in this request. Canonical values, + // smallest to largest: "card", "featured", "large", "full", "original". Empty + // means the plugin default. The vocabulary is open and grows additively, so a + // plugin receiving an unknown variant must degrade gracefully to its nearest + // supported size and must not return an error. See + // pkg/pluginsdk/imagevariant for constants and the full rules. + Variant string `protobuf:"bytes,2,opt,name=variant,proto3" json:"variant,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } diff --git a/pkg/pluginproto/silo/plugin/v1/runtime_host.pb.go b/pkg/pluginproto/silo/plugin/v1/runtime_host.pb.go index 11ba83b..a7643a5 100644 --- a/pkg/pluginproto/silo/plugin/v1/runtime_host.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/runtime_host.pb.go @@ -1543,9 +1543,13 @@ type ResolveCatalogImageURLsRequest struct { // backdrop_url values returned by ListLibraryMedia. Paths are host-owned // opaque strings; plugins should pass them back unchanged. Paths []string `protobuf:"bytes,1,rep,name=paths,proto3" json:"paths,omitempty"` - // Optional image variant name understood by the host resolver. Empty uses the - // host default. Common values include "thumbnail" and "original"; unsupported - // variants are handled host-side. + // Optional semantic size hint understood by the host resolver, not an exact + // pixel contract. Canonical values, smallest to largest: "card", "featured", + // "large", "full", "original". Empty uses the host default. The vocabulary is + // open and grows additively; unsupported variants are degraded host-side to + // the nearest available size rather than erroring, and the same graceful + // degradation is required of plugins that resolve variants themselves. See + // pkg/pluginsdk/imagevariant for constants and the full rules. Variant string `protobuf:"bytes,2,opt,name=variant,proto3" json:"variant,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/pkg/pluginsdk/imagevariant/imagevariant.go b/pkg/pluginsdk/imagevariant/imagevariant.go new file mode 100644 index 0000000..ec5df5c --- /dev/null +++ b/pkg/pluginsdk/imagevariant/imagevariant.go @@ -0,0 +1,50 @@ +// Package imagevariant exposes the canonical image-variant hints Silo sends to +// plugins that resolve image paths into URLs. +// +// A variant is a semantic size hint, not an exact pixel contract: the host says +// roughly how large the image will be rendered and the plugin picks the closest +// size its upstream offers. Variants travel as plain strings in +// ResolveImageURLRequest.variant, ResolveImageURLsRequest.variant, and +// ResolveCatalogImageURLsRequest.variant. An empty variant means "host default". +// +// Two contract rules govern the vocabulary: +// +// 1. The set is open and may grow additively. New variants are added between or +// beyond the existing bands as Silo's clients gain new rendering surfaces; +// adding one is not a breaking change, so a plugin must never assume the +// constants in this package are exhaustive. +// 2. A plugin receiving an unknown variant MUST degrade gracefully to its +// nearest supported size (or its default) and MUST NOT return an error. A +// slightly wrong image size is always preferable to a failed resolve, which +// surfaces to users as a missing poster. +// +// The idiomatic implementation is a switch with a default arm that returns the +// plugin's fallback size, so unrecognized values fall through instead of failing. +package imagevariant + +const ( + // Card is the smallest band: dense grid and list thumbnails, search + // results, and cast rows. Targets roughly 300px-wide posters. + Card = "card" + + // Featured is the mid band: hero artwork, detail-page posters, and section + // headers. Targets roughly 500px-wide posters and logos and 1280px-wide + // backdrops. + Featured = "featured" + + // Large sits between Featured and Full for high-density displays and + // client-selected larger image sizes. Targets roughly 780px-wide posters + // and stills and 1280px-wide logos and backdrops. + Large = "large" + + // Full is the largest sized band, for full-screen and near-source + // rendering. Targets the largest sized rendition the upstream offers, + // falling back to the original asset when no sized rendition is large + // enough. + Full = "full" + + // Original requests the unresized source asset, with no host or plugin + // downscaling. It is also the conventional fallback for an empty or + // unrecognized variant. + Original = "original" +) diff --git a/pkg/pluginsdk/imagevariant/imagevariant_test.go b/pkg/pluginsdk/imagevariant/imagevariant_test.go new file mode 100644 index 0000000..ff2aab8 --- /dev/null +++ b/pkg/pluginsdk/imagevariant/imagevariant_test.go @@ -0,0 +1,21 @@ +package imagevariant + +import "testing" + +func TestCanonicalValues(t *testing.T) { + for _, tc := range []struct { + name string + got string + want string + }{ + {"Card", Card, "card"}, + {"Featured", Featured, "featured"}, + {"Large", Large, "large"}, + {"Full", Full, "full"}, + {"Original", Original, "original"}, + } { + if tc.got != tc.want { + t.Errorf("%s = %q, want %q", tc.name, tc.got, tc.want) + } + } +} diff --git a/proto/silo/plugin/v1/metadata_provider.proto b/proto/silo/plugin/v1/metadata_provider.proto index 1df8018..1e6004f 100644 --- a/proto/silo/plugin/v1/metadata_provider.proto +++ b/proto/silo/plugin/v1/metadata_provider.proto @@ -244,6 +244,12 @@ message GetImagesResponse { message ResolveImageURLRequest { string path = 1; + // Semantic size hint, not an exact pixel contract. Canonical values, smallest + // to largest: "card", "featured", "large", "full", "original". Empty means + // the plugin default. The vocabulary is open and grows additively, so a + // plugin receiving an unknown variant must degrade gracefully to its nearest + // supported size and must not return an error. See + // pkg/pluginsdk/imagevariant for constants and the full rules. string variant = 2; } @@ -253,6 +259,12 @@ message ResolveImageURLResponse { message ResolveImageURLsRequest { repeated string paths = 1; + // Semantic size hint applied to every path in this request. Canonical values, + // smallest to largest: "card", "featured", "large", "full", "original". Empty + // means the plugin default. The vocabulary is open and grows additively, so a + // plugin receiving an unknown variant must degrade gracefully to its nearest + // supported size and must not return an error. See + // pkg/pluginsdk/imagevariant for constants and the full rules. string variant = 2; } diff --git a/proto/silo/plugin/v1/runtime_host.proto b/proto/silo/plugin/v1/runtime_host.proto index e316fc6..5932819 100644 --- a/proto/silo/plugin/v1/runtime_host.proto +++ b/proto/silo/plugin/v1/runtime_host.proto @@ -232,9 +232,13 @@ message ResolveCatalogImageURLsRequest { // backdrop_url values returned by ListLibraryMedia. Paths are host-owned // opaque strings; plugins should pass them back unchanged. repeated string paths = 1; - // Optional image variant name understood by the host resolver. Empty uses the - // host default. Common values include "thumbnail" and "original"; unsupported - // variants are handled host-side. + // Optional semantic size hint understood by the host resolver, not an exact + // pixel contract. Canonical values, smallest to largest: "card", "featured", + // "large", "full", "original". Empty uses the host default. The vocabulary is + // open and grows additively; unsupported variants are degraded host-side to + // the nearest available size rather than erroring, and the same graceful + // degradation is required of plugins that resolve variants themselves. See + // pkg/pluginsdk/imagevariant for constants and the full rules. string variant = 2; } From 8ef0e2098696c51351ff7352d86bda750965862a Mon Sep 17 00:00:00 2001 From: Quick104 <31828688+Quick104@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:56:44 -0400 Subject: [PATCH 07/17] docs(imagevariant): state the receiver-specific empty-variant default An empty variant resolves to the plugin default on the metadata-provider RPCs and the host default on the RuntimeHost RPC; the package doc claimed "host default" for all three. Co-Authored-By: Claude Fable 5 --- pkg/pluginsdk/imagevariant/imagevariant.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/pluginsdk/imagevariant/imagevariant.go b/pkg/pluginsdk/imagevariant/imagevariant.go index ec5df5c..9902e5c 100644 --- a/pkg/pluginsdk/imagevariant/imagevariant.go +++ b/pkg/pluginsdk/imagevariant/imagevariant.go @@ -1,11 +1,13 @@ // Package imagevariant exposes the canonical image-variant hints Silo sends to // plugins that resolve image paths into URLs. // -// A variant is a semantic size hint, not an exact pixel contract: the host says -// roughly how large the image will be rendered and the plugin picks the closest -// size its upstream offers. Variants travel as plain strings in +// A variant is a semantic size hint, not an exact pixel contract: the sender +// says roughly how large the image will be rendered and the resolver picks the +// closest size its upstream offers. Variants travel as plain strings in // ResolveImageURLRequest.variant, ResolveImageURLsRequest.variant, and -// ResolveCatalogImageURLsRequest.variant. An empty variant means "host default". +// ResolveCatalogImageURLsRequest.variant. An empty variant uses the receiver's +// default: the plugin default for the metadata-provider requests and the host +// default for the RuntimeHost request. // // Two contract rules govern the vocabulary: // From d3465c8ac3aa4b21d10300348a21d5a7979aaa77 Mon Sep 17 00:00:00 2001 From: Quick104 <31828688+Quick104@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:09:50 -0400 Subject: [PATCH 08/17] docs(metadata): tighten season_number presence contract and dedupe tests Address review findings on the season-scoped image gallery contract: document the Specials/season-zero presence rule on ImageRecord and the scope-not-guarantee invariant on GetImagesRequest, add compatibility guidance for presence-sensitive optional fields, replace the tautological absence assertion with an empty-message round trip, and consolidate the field-number and optional-int32 presence checks into shared test helpers. Co-Authored-By: Claude Fable 5 --- README.md | 4 + docs/compatibility.md | 16 ++++ .../silo/plugin/v1/metadata_provider.pb.go | 7 +- .../silo/plugin/v1/metadata_provider_test.go | 83 ++++++++----------- .../silo/plugin/v1/optional_presence_test.go | 36 ++++++++ .../plugin/v1/watch_sync_provider_test.go | 31 ++----- proto/silo/plugin/v1/metadata_provider.proto | 7 +- 7 files changed, 110 insertions(+), 74 deletions(-) create mode 100644 pkg/pluginproto/silo/plugin/v1/optional_presence_test.go diff --git a/README.md b/README.md index b1fe23f..c6b0f0e 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,10 @@ convergent desired-state updates rather than increments. That rule also applies to scrobble stops: replaying the same event ID must not create another play. For playback events, `completed` is the host's authoritative watched decision; plugins must not infer completion from `watch_history_id` or percentage alone. +Metadata consumers must likewise check optional `season_number` presence: zero +means Specials, while absence means no season scope. See +[compatibility guidance](docs/compatibility.md#presence-sensitive-optional-fields) +for the request and record rules. Authenticated RPCs receive the same host-owned capability, configuration, and credential data through `WatchSyncAuthenticatedContext`. The context exists diff --git a/docs/compatibility.md b/docs/compatibility.md index a2502d4..d7d5ee3 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -29,6 +29,22 @@ The compatibility boundary includes: - CI and release pipelines should build with `GOWORK=off` and without checking out this repo as a sibling source dependency. - Local `go.work` files and temporary `replace` directives are acceptable for development, but they must not be committed as the release path. +## Presence-Sensitive Optional Fields + +Some contract fields use proto3 `optional` because absence and zero have +different meanings. Current examples are `WatchSyncEvent.list_position`, +`GetImagesRequest.season_number`, and `ImageRecord.season_number`. + +Consumers must check presence (`nil` pointer in Go or `HasField` in reflective +APIs), never infer it from the numeric value. Calling +`GetSeasonNumber() != 0` silently conflates "no season scope" with "Specials +(season zero) requested." + +A season-scoped `GetImagesRequest` is a scope, not a guarantee. Plugins that +can filter by season should do so, and plugins should populate +`ImageRecord.season_number` whenever the season is known. Hosts must bucket and +verify images by the per-image field rather than assume a filtered response. + ## Runtime Compatibility - `silo_api_version` is the coarse runtime compatibility gate between Silo and a plugin binary. diff --git a/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go b/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go index 31cd31d..7e7335f 100644 --- a/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go @@ -1688,6 +1688,9 @@ type ImageRecord struct { Height int32 `protobuf:"varint,5,opt,name=height,proto3" json:"height,omitempty"` Metadata *structpb.Struct `protobuf:"bytes,6,opt,name=metadata,proto3" json:"metadata,omitempty"` // Present only when the image is confirmed to belong to one exact season. + // Season zero represents Specials, so presence must not be inferred from the + // numeric value. Plugins should populate this field whenever the season is + // known, even when the request was already season-scoped. SeasonNumber *int32 `protobuf:"varint,7,opt,name=season_number,json=seasonNumber,proto3,oneof" json:"season_number,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1780,7 +1783,9 @@ type GetImagesRequest struct { Language string `protobuf:"bytes,4,opt,name=language,proto3" json:"language,omitempty"` // Present only when requesting artwork for a specific TV season. // Season zero represents Specials, so presence must not be inferred from - // the numeric value. + // the numeric value. This scope is a request, not a guarantee: plugins that + // can filter by season should do so, but hosts must verify season attribution + // via ImageRecord.season_number rather than assume a filtered response. SeasonNumber *int32 `protobuf:"varint,5,opt,name=season_number,json=seasonNumber,proto3,oneof" json:"season_number,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/pkg/pluginproto/silo/plugin/v1/metadata_provider_test.go b/pkg/pluginproto/silo/plugin/v1/metadata_provider_test.go index c6fb3a3..f47c4f8 100644 --- a/pkg/pluginproto/silo/plugin/v1/metadata_provider_test.go +++ b/pkg/pluginproto/silo/plugin/v1/metadata_provider_test.go @@ -7,6 +7,14 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" ) +func assertFieldNumber(t *testing.T, message protoreflect.ProtoMessage, field string, want protoreflect.FieldNumber) { + t.Helper() + got := message.ProtoReflect().Descriptor().Fields().ByName(protoreflect.Name(field)) + if got == nil || got.Number() != want { + t.Fatalf("%T.%s field number = %v, want %d", message, field, got, want) + } +} + func TestMetadataItemDescriptor_IncludesReleaseDate(t *testing.T) { field := (&MetadataItem{}).ProtoReflect().Descriptor().Fields().ByName("release_date") if field == nil { @@ -38,21 +46,14 @@ func TestMetadataTitleDescriptors_IncludeAliasAndLanguageFields(t *testing.T) { } } - assertFieldNumber := func(message protoreflect.ProtoMessage, field string, want protoreflect.FieldNumber) { - t.Helper() - got := message.ProtoReflect().Descriptor().Fields().ByName(protoreflect.Name(field)) - if got == nil || got.Number() != want { - t.Fatalf("%T.%s field number = %v, want %d", message, field, got, want) - } - } - assertFieldNumber(&ProviderSearchResult{}, "title_aliases", 9) - assertFieldNumber(&ProviderSearchResult{}, "title_language", 10) - assertFieldNumber(&ProviderSearchResult{}, "title_is_fallback", 11) - assertFieldNumber(&ProviderSearchResult{}, "original_language", 12) - assertFieldNumber(&MetadataItem{}, "title_aliases", 33) - assertFieldNumber(&MetadataItem{}, "title_language", 34) - assertFieldNumber(&MetadataItem{}, "title_is_fallback", 35) - assertFieldNumber(&MetadataItem{}, "title_aliases_complete", 36) + assertFieldNumber(t, &ProviderSearchResult{}, "title_aliases", 9) + assertFieldNumber(t, &ProviderSearchResult{}, "title_language", 10) + assertFieldNumber(t, &ProviderSearchResult{}, "title_is_fallback", 11) + assertFieldNumber(t, &ProviderSearchResult{}, "original_language", 12) + assertFieldNumber(t, &MetadataItem{}, "title_aliases", 33) + assertFieldNumber(t, &MetadataItem{}, "title_language", 34) + assertFieldNumber(t, &MetadataItem{}, "title_is_fallback", 35) + assertFieldNumber(t, &MetadataItem{}, "title_aliases_complete", 36) } func TestMetadataItem_OldPayloadDecodesWithOptionalTitleFields(t *testing.T) { @@ -156,44 +157,28 @@ func TestMetadataProviderRequestDescriptors_IncludeProviderContext(t *testing.T) func TestGetImagesRequest_SeasonNumberPreservesPresenceAndSpecials(t *testing.T) { seasonZero := int32(0) - payload, err := proto.Marshal(&GetImagesRequest{SeasonNumber: &seasonZero}) - if err != nil { - t.Fatalf("marshal request: %v", err) - } - - var decoded GetImagesRequest - if err := proto.Unmarshal(payload, &decoded); err != nil { - t.Fatalf("unmarshal request: %v", err) - } - if decoded.SeasonNumber == nil { - t.Fatal("season_number presence was lost for Specials") - } - if decoded.GetSeasonNumber() != 0 { - t.Fatalf("season_number = %d, want 0", decoded.GetSeasonNumber()) - } - if (&GetImagesRequest{}).SeasonNumber != nil { - t.Fatal("season_number should be absent for item-level image requests") - } + assertOptionalInt32Presence( + t, + &GetImagesRequest{SeasonNumber: &seasonZero}, + &GetImagesRequest{}, + func(message proto.Message) *int32 { + return message.(*GetImagesRequest).SeasonNumber + }, + ) + assertFieldNumber(t, &GetImagesRequest{}, "season_number", 5) } func TestImageRecord_SeasonNumberPreservesPresenceAndSpecials(t *testing.T) { seasonZero := int32(0) - payload, err := proto.Marshal(&ImageRecord{SeasonNumber: &seasonZero}) - if err != nil { - t.Fatalf("marshal image: %v", err) - } - - var decoded ImageRecord - if err := proto.Unmarshal(payload, &decoded); err != nil { - t.Fatalf("unmarshal image: %v", err) - } - if decoded.SeasonNumber == nil || decoded.GetSeasonNumber() != 0 { - t.Fatalf("season_number = %v, want present Specials value 0", decoded.SeasonNumber) - } - field := decoded.ProtoReflect().Descriptor().Fields().ByName("season_number") - if field == nil || field.Number() != 7 { - t.Fatalf("season_number descriptor = %v, want field 7", field) - } + assertOptionalInt32Presence( + t, + &ImageRecord{SeasonNumber: &seasonZero}, + &ImageRecord{}, + func(message proto.Message) *int32 { + return message.(*ImageRecord).SeasonNumber + }, + ) + assertFieldNumber(t, &ImageRecord{}, "season_number", 7) } func TestMetadataProviderServiceDescriptor_IncludesPersonDetailRPC(t *testing.T) { diff --git a/pkg/pluginproto/silo/plugin/v1/optional_presence_test.go b/pkg/pluginproto/silo/plugin/v1/optional_presence_test.go new file mode 100644 index 0000000..38c089a --- /dev/null +++ b/pkg/pluginproto/silo/plugin/v1/optional_presence_test.go @@ -0,0 +1,36 @@ +package pluginv1 + +import ( + "testing" + + "google.golang.org/protobuf/proto" +) + +func assertOptionalInt32Presence(t *testing.T, withZero proto.Message, empty proto.Message, get func(proto.Message) *int32) { + t.Helper() + + data, err := proto.Marshal(withZero) + if err != nil { + t.Fatalf("marshal %T with explicit zero: %v", withZero, err) + } + decoded := withZero.ProtoReflect().New().Interface() + if err := proto.Unmarshal(data, decoded); err != nil { + t.Fatalf("unmarshal %T with explicit zero: %v", withZero, err) + } + value := get(decoded) + if value == nil || *value != 0 { + t.Fatalf("%T explicit zero = %v, want present value 0", decoded, value) + } + + data, err = proto.Marshal(empty) + if err != nil { + t.Fatalf("marshal empty %T: %v", empty, err) + } + decoded = empty.ProtoReflect().New().Interface() + if err := proto.Unmarshal(data, decoded); err != nil { + t.Fatalf("unmarshal empty %T: %v", empty, err) + } + if value := get(decoded); value != nil { + t.Fatalf("%T absent value = %v, want nil", decoded, value) + } +} diff --git a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go index 4e21030..2c1d4f1 100644 --- a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go +++ b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go @@ -131,29 +131,14 @@ func TestWatchSyncDeviceAuthorizationPendingStatePreservesPresence(t *testing.T) func TestWatchSyncListPositionPreservesPresence(t *testing.T) { zero := int32(0) - withZero := &WatchSyncEvent{ListPosition: &zero} - data, err := proto.Marshal(withZero) - if err != nil { - t.Fatal(err) - } - var output WatchSyncEvent - if err := proto.Unmarshal(data, &output); err != nil { - t.Fatal(err) - } - if output.ListPosition == nil || output.GetListPosition() != 0 { - t.Fatalf("explicit zero list position = %#v", output.ListPosition) - } - omittedData, err := proto.Marshal(&WatchSyncEvent{}) - if err != nil { - t.Fatal(err) - } - var omittedOutput WatchSyncEvent - if err := proto.Unmarshal(omittedData, &omittedOutput); err != nil { - t.Fatal(err) - } - if omittedOutput.ListPosition != nil { - t.Fatal("omitted list position unexpectedly has presence") - } + assertOptionalInt32Presence( + t, + &WatchSyncEvent{ListPosition: &zero}, + &WatchSyncEvent{}, + func(message proto.Message) *int32 { + return message.(*WatchSyncEvent).ListPosition + }, + ) } func TestWatchSyncListTombstoneDoesNotRequireMedia(t *testing.T) { diff --git a/proto/silo/plugin/v1/metadata_provider.proto b/proto/silo/plugin/v1/metadata_provider.proto index 8e9f582..c290f90 100644 --- a/proto/silo/plugin/v1/metadata_provider.proto +++ b/proto/silo/plugin/v1/metadata_provider.proto @@ -230,6 +230,9 @@ message ImageRecord { int32 height = 5; google.protobuf.Struct metadata = 6; // Present only when the image is confirmed to belong to one exact season. + // Season zero represents Specials, so presence must not be inferred from the + // numeric value. Plugins should populate this field whenever the season is + // known, even when the request was already season-scoped. optional int32 season_number = 7; } @@ -240,7 +243,9 @@ message GetImagesRequest { string language = 4; // Present only when requesting artwork for a specific TV season. // Season zero represents Specials, so presence must not be inferred from - // the numeric value. + // the numeric value. This scope is a request, not a guarantee: plugins that + // can filter by season should do so, but hosts must verify season attribution + // via ImageRecord.season_number rather than assume a filtered response. optional int32 season_number = 5; } From 275ea3ddfd4d1c5304d6994161cf177f71bd19ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 00:31:27 +0000 Subject: [PATCH 09/17] build(deps): bump golang.org/x/net from 0.53.0 to 0.55.0 Bumps [golang.org/x/net](https://github.com/golang/net) from 0.53.0 to 0.55.0. - [Commits](https://github.com/golang/net/compare/v0.53.0...v0.55.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.55.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 64e39e2..7ad623f 100644 --- a/go.mod +++ b/go.mod @@ -17,8 +17,8 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/oklog/run v1.1.0 // indirect - golang.org/x/net v0.53.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.36.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect ) diff --git a/go.sum b/go.sum index 6681331..ac30320 100644 --- a/go.sum +++ b/go.sum @@ -55,18 +55,18 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= From f110653047449de7220f8fb9f7ce49ddecc7d9a0 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:21:42 -0400 Subject: [PATCH 10/17] docs: standardize contribution guidance (#18) --- .gitignore | 4 +++- AGENTS.md | 15 +++++++++++++ CLAUDE.md | 1 + CONTRIBUTING.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 18 ++++++++++++++-- 5 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 AGENTS.md create mode 120000 CLAUDE.md create mode 100644 CONTRIBUTING.md diff --git a/.gitignore b/.gitignore index 9d94932..d1c5689 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,9 @@ go.work.sum *.log logs/ .DS_Store -.claude/ +# Local agent/tooling state +/.agents/ +/.claude/ .codex/ .cursor/ .playwright-mcp/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d8580aa --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,15 @@ +# Agent instructions + +Read [README.md](README.md) and [CONTRIBUTING.md](CONTRIBUTING.md) before working here; they define this repository's ownership, build, and validation rules. + +## Writing + +Give human-facing prose a final readability pass. Lead with the outcome, use concrete plain language and active voice, and cut filler, stock framing, repetition, and promotional claims. Preserve meaning, evidence, citations, uncertainty, and established terminology. Never rewrite exact quotations, commands, logs, identifiers, API names, or contractual language. Match the audience and use restrained formatting. + +## Pull requests + +- Never create a pull request unless the developer explicitly asks. +- Use a plain-language Conventional Commit title. In the body, explain the problem before the solution and end with the required AI disclosure, naming the exact model, harness, and tooling used. +- For UI changes, include before-and-after evidence and a video for motion or timing behavior. Upload pull-request evidence to GitHub; never commit PR-only assets such as `.github/pr-assets`. +- Keep one concern per pull request. If the description needs the word "also" for another change, split it. +- When babysitting a pull request, poll for checks and comments newer than the last push. Verify bot findings against the source, fix real issues, and dismiss false positives with a written reason. Stay quiet when nothing is new, and stop when the latest commit is green. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..695a6a7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing to the Silo Plugin SDK + +The [Silo contribution guide](https://github.com/Silo-Server/.github/blob/main/CONTRIBUTING.md) +covers project-wide coordination, focused changes, evidence, AI disclosure, and +pull request expectations. Those requirements apply here; this guide adds the +SDK-specific workflow. + +## Before you start + +Open an [issue](https://github.com/Silo-Server/silo-plugin-sdk/issues) before +adding or changing a capability, protobuf contract, manifest field, runtime +behavior, or public Go API. The SDK is a versioned contract consumed by +`silo-server` and every plugin, so identify affected downstream repositories and +the intended compatibility strategy before implementation. + +Host-only behavior belongs in +[`silo-server`](https://github.com/Silo-Server/silo-server); provider-specific +behavior belongs in the individual plugin repository. + +## Development setup + +Use the Go version declared in `go.mod`. Protobuf regeneration also requires +`protoc`; `make proto` installs the generator tools under `bin/` as needed. +Read [docs/compatibility.md](docs/compatibility.md) before changing a public +contract. + +## Validate your change + +Run focused package tests while iterating. Before opening a pull request, run: + +```sh +go test ./... +go vet ./... +go build ./examples/hello-scheduled-task +go build ./examples/hello-runtime-host +gofmt -l . +``` + +For protobuf or generated-code changes, also run: + +```sh +make proto +``` + +`gofmt -l .` should print nothing. If it reports unrelated pre-existing drift, +none of the Go files touched by your change may appear in the output; do not add +to the output, and report what remains. When no contract change is intended, +`make proto` must leave generated code unchanged. Add compatibility coverage +for presence semantics, validation, and existing consumers when a contract +changes. + +## Open the pull request + +Use a Conventional Commit title, explain the compatibility and release impact, +list downstream coordination, and paste the actual validation results. Read the +[AI-assisted contribution policy](https://github.com/Silo-Server/silo-server/blob/main/docs/ai-contributions.md) +and include its disclosure block. diff --git a/README.md b/README.md index c6b0f0e..7ef173b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ Public Go SDK for building Silo plugins. **Not a runtime plugin** — this is a library that plugin authors depend on via `go.mod`. -`silo-plugin-sdk` is the source of truth for the plugin authoring contract. First-party consumers (Silo host, `silo-plugin-tmdb`, `silo-plugin-metadb`, every other plugin in this repo) pin tagged semver releases. Local multi-repo workspaces may use `go.work` or a temporary `replace`, but CI and release builds resolve the SDK from a published module tag. +`silo-plugin-sdk` is the source of truth for the plugin authoring contract. +First-party consumers—including the Silo host and the separate metadata, +marker, autoscan, and watch-provider plugin repositories—pin tagged semantic +versions. Local multi-repository workspaces may use `go.work`, but CI and +release builds resolve the SDK from a published module tag. ## Packages @@ -10,6 +14,8 @@ Public Go SDK for building Silo plugins. **Not a runtime plugin** — this is a - `github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/capability` — stable capability type constants for manifests and peer discovery. - `github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/config` — config-schema helpers. - `github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/convert` — type conversions. +- `github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/httpclient` — credentialed JSON-over-HTTP client with bounded responses and typed status errors. +- `github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/imagevariant` — canonical image-size variant strings. - `github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/manifest` — manifest loading/rendering. - `github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/runtime` — `manifest` subcommand + `Runtime` server scaffolding. - `github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/runtimedefault` — default `Runtime` implementation with `BindHostBroker` already wired; embed it to skip boilerplate. @@ -22,6 +28,7 @@ The SDK ships protobuf contracts for every capability the host understands: - `metadata_provider.v1` - `marker_provider.v1` - `media_analyzer.v1` +- `image_resolver.v1` - `scheduled_task.v1` - `event_consumer.v1` - `auth_provider.v1` @@ -230,10 +237,17 @@ Before downstream repos stop using local workspace overrides, the required SDK c ## Build & test ```bash -make proto # regenerate protobuf code (uses locally vendored buf under ./bin/) +make proto # regenerate protobuf code (installs tools under ./bin/ as needed) go test ./... ``` +## Contributing + +Read [CONTRIBUTING.md](CONTRIBUTING.md) and +[docs/compatibility.md](docs/compatibility.md) before opening a pull request. +Public Go, protobuf, runtime, and manifest changes should start as an issue and +identify affected downstream repositories. + ## License Apache-2.0. See [LICENSE](LICENSE). From e873349c768bb8bce1dd628affae2e46e2c6fa88 Mon Sep 17 00:00:00 2001 From: Quick104 <31828688+Quick104@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:28:40 -0400 Subject: [PATCH 11/17] docs: add naming and branding guidance for plugin authors Follows Silo-Server/siloserver.org#16, which makes the policy in that repository canonical and adds naming, writing, and colour guidance. Co-Authored-By: Claude Fable 5.1 --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 7ef173b..864d133 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,17 @@ Read [CONTRIBUTING.md](CONTRIBUTING.md) and Public Go, protobuf, runtime, and manifest changes should start as an issue and identify affected downstream repositories. +## Naming and branding + +Give your plugin its own name and mention Silo in the summary, for example +"Trakt sync plugin for Silo". Repository and package names such as +`silo-plugin-trakt` are fine, since "silo" only describes what the code plugs +into. Avoid "Silo[word]" product names such as SiloTrakt, which read as +official, and do not use the Silo logo as your plugin's icon. Set +`publisher_name` to yourself, not "Silo", unless the plugin is published by the +project. The full guidance, including what needs no permission, is at +. + ## License Apache-2.0. See [LICENSE](LICENSE). From cc5b2e34f87bc55841534618ff31d07381037933 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:10:25 +0000 Subject: [PATCH 12/17] feat(network-access): add the network_access_provider.v1 capability Silo-Server/silo-server#1001 settled on shipping overlay-network access (Tailscale via tsnet first) as a plugin that owns the overlay listener and reverse-proxies to the host. The SDK had no capability for it and no way for a plugin to learn the host's local listeners, keep per-instance state, or push status. Add the NetworkAccessProvider service (Connect, Disconnect, GetStatus), a typed NetworkAccessProviderDescriptor on CapabilityDescriptor (field 11), and three RuntimeHost additions: GetHostInfo gains host role, host name, node id, an ingress token, and the listeners to expose; ReadInstanceState and WriteInstanceState give each plugin instance a host-scoped encrypted key-value store sized for tsnet node state; ReportNetworkAccessStatus pushes state changes. runtimehost ships an InstanceStateStore matching the two-method shape of tailscale's ipn.StateStore without importing tailscale, plus typed HostInfo fields and state constants. Manifest validation requires the descriptor on the capability and a path-safe provider slug. A stub example plugin exercises the contract without a tsnet dependency so silo-server can use it as a test fixture. All proto changes are additive. CapabilityServers gains a thirteenth field, so plugins using an unkeyed composite literal must switch to keyed fields; the compat test documents this. Co-Authored-By: Claude Fable 5.1 --- CONTRIBUTING.md | 1 + README.md | 13 +- docs/network-access-provider.md | 180 +++++ docs/runtime-host.md | 5 +- examples/hello-network-access/.gitignore | 1 + examples/hello-network-access/README.md | 27 + examples/hello-network-access/main.go | 166 ++++ examples/hello-network-access/manifest.json | 18 + pkg/pluginproto/silo/plugin/v1/common.pb.go | 115 +-- .../plugin/v1/network_access_provider.pb.go | 454 +++++++++++ .../v1/network_access_provider_grpc.pb.go | 219 ++++++ .../silo/plugin/v1/runtime_host.pb.go | 739 ++++++++++++++---- .../silo/plugin/v1/runtime_host_grpc.pb.go | 132 ++++ pkg/pluginsdk/capability/capability.go | 4 + pkg/pluginsdk/capability/capability_test.go | 16 + pkg/pluginsdk/manifest/manifest.go | 20 + .../manifest/network_access_provider_test.go | 62 ++ .../runtime/capability_servers_compat_test.go | 8 +- .../runtime/network_access_provider_test.go | 44 ++ pkg/pluginsdk/runtime/runtime.go | 11 + pkg/pluginsdk/runtimehost/client_test.go | 46 ++ pkg/pluginsdk/runtimehost/host_info.go | 72 +- pkg/pluginsdk/runtimehost/instance_state.go | 130 +++ .../runtimehost/instance_state_test.go | 140 ++++ pkg/pluginsdk/runtimehost/network_access.go | 32 + proto/silo/plugin/v1/common.proto | 6 + .../plugin/v1/network_access_provider.proto | 72 ++ proto/silo/plugin/v1/runtime_host.proto | 70 ++ 28 files changed, 2590 insertions(+), 213 deletions(-) create mode 100644 docs/network-access-provider.md create mode 100644 examples/hello-network-access/.gitignore create mode 100644 examples/hello-network-access/README.md create mode 100644 examples/hello-network-access/main.go create mode 100644 examples/hello-network-access/manifest.json create mode 100644 pkg/pluginproto/silo/plugin/v1/network_access_provider.pb.go create mode 100644 pkg/pluginproto/silo/plugin/v1/network_access_provider_grpc.pb.go create mode 100644 pkg/pluginsdk/manifest/network_access_provider_test.go create mode 100644 pkg/pluginsdk/runtime/network_access_provider_test.go create mode 100644 pkg/pluginsdk/runtimehost/instance_state.go create mode 100644 pkg/pluginsdk/runtimehost/instance_state_test.go create mode 100644 pkg/pluginsdk/runtimehost/network_access.go create mode 100644 proto/silo/plugin/v1/network_access_provider.proto diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 695a6a7..eda31dc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,6 +33,7 @@ go test ./... go vet ./... go build ./examples/hello-scheduled-task go build ./examples/hello-runtime-host +go build ./examples/hello-network-access gofmt -l . ``` diff --git a/README.md b/README.md index 864d133..9914794 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ The SDK ships protobuf contracts for every capability the host understands: - `request_router.v1` - `scan_source.v1` - `watch_sync_provider.v1` +- `network_access_provider.v1` - `audiobook_backend.v1` - `ebook_backend.v1` @@ -50,7 +51,7 @@ A typical plugin: 3. Supports the `manifest` subcommand via `pkg/pluginsdk/runtime` so the host can introspect manifests without launching the plugin. 4. Is installed either from a catalog or by uploading a trusted binary to a Silo server. -For a minimal self-describing plugin, see [`examples/hello-scheduled-task`](examples/hello-scheduled-task). For a plugin that calls back into the host via `RuntimeHost` (publishing events, listing libraries), see [`examples/hello-runtime-host`](examples/hello-runtime-host). +For a minimal self-describing plugin, see [`examples/hello-scheduled-task`](examples/hello-scheduled-task). For a plugin that calls back into the host via `RuntimeHost` (publishing events, listing libraries), see [`examples/hello-runtime-host`](examples/hello-runtime-host). For a stub overlay-network provider, see [`examples/hello-network-access`](examples/hello-network-access). ## Operator-facing presentation @@ -204,6 +205,16 @@ and the order of returned watchlist states is the remote list order. Event `list_position` is presence-aware: an explicit zero means the first position, while omission means no requested ordering. +## Network access providers + +`network_access_provider.v1` lets a resident plugin give the deployment an +overlay-network identity (Tailscale, NetBird) and reverse-proxy overlay +traffic to the host's local listeners. The host starts these plugins at boot, +restarts them on crash, stores their per-instance state encrypted, and +aggregates status across the API server and proxy nodes. See +[docs/network-access-provider.md](docs/network-access-provider.md) for the +proxy contract, `GetHostInfo` fields, instance state, and enrollment rules. + ## Scan sources The `scan_source.v1` capability is for Autoscan providers. The host owns the diff --git a/docs/network-access-provider.md b/docs/network-access-provider.md new file mode 100644 index 0000000..d4f3ae3 --- /dev/null +++ b/docs/network-access-provider.md @@ -0,0 +1,180 @@ +# Network access providers (`network_access_provider.v1`) + +A network access provider gives a Silo deployment an overlay-network identity +(Tailscale via tsnet first; NetBird later) so clients reach Silo without port +forwarding or a public reverse proxy. The plugin owns the overlay client and a +reverse proxy in front of the host's local listeners. The host owns +supervision, per-instance state storage, status aggregation, access-path +routing, and the admin API. + +Added in SDK v0.16.0. Server support arrives in stages; check the host's +`/api/v2/network-access/capabilities` before assuming a feature exists. + +## Manifest + +Declare the capability with the typed descriptor. The host lists providers +from the descriptor without launching the plugin. + +```json +{ + "type": "network_access_provider.v1", + "id": "tailscale", + "display_name": "Tailscale", + "network_access_provider": { + "provider": "tailscale", + "display_name": "Tailscale" + } +} +``` + +`provider` is a stable lowercase, path-safe slug; it appears in admin API +paths such as `/api/v2/admin/network-access/{provider}/status`. Reserved +slugs today: `tailscale`, `netbird`. + +## Resident lifecycle + +Plugins declaring this capability are resident. The host: + +- starts them when the API listener binds (and on every proxy node), +- restarts them on exit or failed health with exponential backoff, +- stops them last during shutdown. + +Every host (the API server and each proxy node) runs its own instance of the +same installation. Each instance has its own overlay identity and its own +instance-state scope. Do not assume a single process. + +## gRPC service + +```proto +service NetworkAccessProvider { + rpc Connect(NetworkAccessConnectRequest) returns (NetworkAccessStatus); + rpc Disconnect(NetworkAccessDisconnectRequest) returns (NetworkAccessStatus); + rpc GetStatus(NetworkAccessGetStatusRequest) returns (NetworkAccessStatus); +} +``` + +Register it with `runtime.CapabilityServers{NetworkAccessProvider: ...}`. + +`NetworkAccessStatus.state` is one of `disconnected`, +`awaiting_authorization`, `connecting`, `connected`, `error`. The vocabulary +is open; the host tolerates values it does not know. `Connect` may return +`awaiting_authorization` or `connecting` and finish enrollment in the +background; push each later transition with +`RuntimeHost.ReportNetworkAccessStatus` so the host does not poll. The host +still calls `GetStatus` on demand. + +`desired_connected` is plugin-owned intent. Persist it in instance state (see +below), report it in every status, and reconnect on start when it is true. +The host never writes it. + +## What the host tells you: `GetHostInfo` + +`RuntimeHost.GetHostInfo` carries everything the proxy needs: + +| Field | Meaning | +|---|---| +| `host_role` | `api` or `proxy`. | +| `host_name` | Node name on proxies, server name on the API host. | +| `node_id` | `stream_nodes.id` on proxies, `0` on the API host. | +| `ingress_token` | Per-process-start secret; see below. | +| `listeners` | Local listeners to expose: `name`, loopback `address`, `default_port`. | + +`listeners` always contains `api`. On the API host it also carries `jellyfin` +and `abs` when those listeners are enabled. Proxies report only `api`. +`default_port` is the port the plugin should expose that listener on (443 for +`api` under HTTPS, 8096 for `jellyfin`, 13378 for `abs`); zero means use the +provider default. Expose every listener the host reports and return one +`NetworkAccessListener` per exposed listener in the status; `origin` is the +`api` listener's origin. + +The typed client is `runtimehost.Client.GetHostInfo`, which returns +`runtimehost.HostInfo` with `Listener(name)` for lookup. + +## Proxy contract + +For every request the plugin forwards to a host listener: + +- Preserve the incoming `Host` header. +- Overwrite `X-Forwarded-Proto` with `https`. +- Set `X-Forwarded-For` to the overlay peer address. +- Set `X-Silo-Ingress-Token` to the value from `GetHostInfo`, replacing any + client-supplied value; the host answers 403 when the header carries more + than one value. + +The host validates the token, strips the header, and records the request's +access path so stream URLs point tailnet clients at overlay origins. The +plugin's loopback source is in the host's default trusted-proxy list, so the +forwarded headers are honoured. + +The ingress token rotates on every host process start and is compared in +constant time. Keep it in memory only; never log or persist it. After a host +or plugin restart, call `GetHostInfo` again before proxying; a stale token is +rejected with `403`. + +## Instance state + +Per-instance state (tsnet node keys, profile state, `desired_connected`) +lives in the host, encrypted, scoped to installation plus host. The plugin +never sees the scope and never writes files. + +```proto +rpc ReadInstanceState(ReadInstanceStateRequest) returns (ReadInstanceStateResponse); +rpc WriteInstanceState(WriteInstanceStateRequest) returns (WriteInstanceStateResponse); +``` + +Limits: key up to 256 bytes, value up to 256 KiB, up to 256 keys per scope. +The SDK client checks the key and value limits before the round trip. + +`runtimehost.InstanceStateStore` exposes the RPCs in the two-method shape of +tailscale's `ipn.StateStore`: + +```go +type StateStore interface { + ReadState(key string) ([]byte, error) // runtimehost.ErrStateNotExist when absent + WriteState(key string, value []byte) error +} +``` + +The SDK does not import tailscale, and `ipn.StateStore` takes `ipn.StateKey` +rather than `string`, so wrap it in the plugin: + +```go +type tsStore struct{ inner *runtimehost.InstanceStateStore } + +func (s tsStore) ReadState(k ipn.StateKey) ([]byte, error) { + b, err := s.inner.ReadState(string(k)) + if errors.Is(err, runtimehost.ErrStateNotExist) { + return nil, ipn.ErrStateNotExist + } + return b, err +} + +func (s tsStore) WriteState(k ipn.StateKey, v []byte) error { + return s.inner.WriteState(string(k), v) +} + +srv := &tsnet.Server{Store: tsStore{inner: host.InstanceStateStore()}} +``` + +Each `ReadState`/`WriteState` call uses a 30 s deadline by default; +`WithTimeout` changes it. + +## Enrollment + +Support both an auth key in plugin config (for hands-off enrollment across +many nodes) and an interactive auth URL as the fallback. While enrollment is +pending, report `state: awaiting_authorization` with `auth_url` set. The +`auth_url` is admin-only: never log it, and clear it once enrollment +completes. + +## Logging + +Log state transitions and errors. Never log `auth_url`, `ingress_token`, +auth keys, or instance state values. + +## Example + +[`examples/hello-network-access`](../examples/hello-network-access) is a stub +provider with no overlay network. It shows the manifest, registration, +instance-state persistence of `desired_connected`, and the status push, and +serves as a test fixture for the host. diff --git a/docs/runtime-host.md b/docs/runtime-host.md index ef040a9..b8f1389 100644 --- a/docs/runtime-host.md +++ b/docs/runtime-host.md @@ -11,7 +11,7 @@ plugin, the plugin calls back into the host. | `PublishEvent(name, payload)` | Publish onto the host's event bus. The host stamps `plugin..` in front of `name` server-side. | | `PublishEventTo(target_plugin_id, name, payload)` | Publish an event addressed to one installed plugin by stable `plugin_id`. | | `PublishEventToInstallation(target_installation_id, name, payload)` | Publish an event addressed to one specific plugin installation. | -| `GetHostInfo()` | Return public-safe host URLs for callbacks and plugin-served links. | +| `GetHostInfo()` | Return public-safe host URLs for callbacks and plugin-served links, plus host role, listeners, and the ingress token for network access providers. | | `ListLibraries(user_id)` | Return libraries (optionally scoped to a user). | | `CheckMediaPresence(provider, media_type, ids)` | Batched lookup: which external IDs already exist in the host catalog. v1 supports provider="tmdb" only. | | `ListInstalledPlugins()` | Return installed plugins and their advertised capabilities. | @@ -21,6 +21,9 @@ plugin, the plugin calls back into the host. | `ResolveCatalogImageURLs(paths, variant)` | Resolve stored catalog image paths into host-generated browser URL targets. | | `MintScopedStream(request)` | Mint a short-lived stream grant for plugin-owned public access workflows. | | `CallPluginHTTP(request)` | Invoke another installed plugin's HTTP route through the host control plane. | +| `ReadInstanceState(key)` | Read one key of the calling instance's encrypted, host-scoped state. | +| `WriteInstanceState(key, value)` | Write one key of that state. Key ≤ 256 bytes, value ≤ 256 KiB, ≤ 256 keys per scope. | +| `ReportNetworkAccessStatus(status)` | Push a `network_access_provider.v1` status change so the host does not poll. | ## Using it from a plugin diff --git a/examples/hello-network-access/.gitignore b/examples/hello-network-access/.gitignore new file mode 100644 index 0000000..79d68cb --- /dev/null +++ b/examples/hello-network-access/.gitignore @@ -0,0 +1 @@ +hello-network-access diff --git a/examples/hello-network-access/README.md b/examples/hello-network-access/README.md new file mode 100644 index 0000000..653b743 --- /dev/null +++ b/examples/hello-network-access/README.md @@ -0,0 +1,27 @@ +# Hello Network Access + +A stub `network_access_provider.v1` plugin with no overlay network. It shows +the contract shape and doubles as a host-side test fixture: + +- `Connect` marks the instance connected, builds a fake origin per listener + reported by `RuntimeHost.GetHostInfo`, persists `desired_connected` in + host-provided instance state, and pushes the status with + `ReportNetworkAccessStatus`. +- `Disconnect` reverses it. +- `GetStatus` restores `desired_connected` from instance state on first call + so intent survives a plugin restart. + +There is no tsnet dependency. A real provider is described in +[docs/network-access-provider.md](../../docs/network-access-provider.md). + +## Build + +```sh +go build -o hello-network-access ./examples/hello-network-access +``` + +## Inspect the manifest + +```sh +./hello-network-access manifest +``` diff --git a/examples/hello-network-access/main.go b/examples/hello-network-access/main.go new file mode 100644 index 0000000..25f897c --- /dev/null +++ b/examples/hello-network-access/main.go @@ -0,0 +1,166 @@ +// Command hello-network-access is a stub network_access_provider.v1 plugin. +// +// It has no overlay network: Connect flips an in-memory state to "connected", +// reports a fake origin built from the host's api listener, persists +// desired_connected in host-provided instance state, and Disconnect reverses +// it. It exists so silo-server tests can exercise the resident plugin +// supervisor, the instance state RPCs, and the status push without a tsnet +// dependency. A real provider follows docs/network-access-provider.md. +package main + +import ( + "context" + _ "embed" + "fmt" + "os" + "sync" + + "github.com/hashicorp/go-hclog" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" + sdkruntime "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/runtime" + "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/runtimehost" +) + +//go:embed manifest.json +var manifestJSON []byte + +const ( + version = "0.1.0" + // desiredKey is the instance state key holding plugin-owned intent. + desiredKey = "desired_connected" +) + +type provider struct { + pluginv1.UnimplementedNetworkAccessProviderServer + + logger hclog.Logger + + mu sync.Mutex + connected bool + desired bool + restored bool + hostInfo *runtimehost.HostInfo +} + +func (p *provider) Connect(ctx context.Context, _ *pluginv1.NetworkAccessConnectRequest) (*pluginv1.NetworkAccessStatus, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.restoreLocked(ctx) + host := sdkruntime.Host() + if host != nil { + info, err := host.GetHostInfo(ctx) + if err != nil { + return nil, fmt.Errorf("get host info: %w", err) + } + p.hostInfo = info + } + p.connected = true + p.desired = true + p.persistLocked(ctx, host) + status := p.statusLocked() + p.reportLocked(ctx, host, status) + return status, nil +} + +func (p *provider) Disconnect(ctx context.Context, _ *pluginv1.NetworkAccessDisconnectRequest) (*pluginv1.NetworkAccessStatus, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.restoreLocked(ctx) + host := sdkruntime.Host() + p.connected = false + p.desired = false + p.persistLocked(ctx, host) + status := p.statusLocked() + p.reportLocked(ctx, host, status) + return status, nil +} + +func (p *provider) GetStatus(ctx context.Context, _ *pluginv1.NetworkAccessGetStatusRequest) (*pluginv1.NetworkAccessStatus, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.restoreLocked(ctx) + return p.statusLocked(), nil +} + +// restoreLocked loads desired_connected from instance state once per process +// and, when it was true, resumes the "connection" so the intent survives +// plugin restarts. A real provider would bring the overlay up here. +func (p *provider) restoreLocked(ctx context.Context) { + if p.restored { + return + } + host := sdkruntime.Host() + if host == nil { + return + } + p.restored = true + value, found, err := host.ReadInstanceState(ctx, desiredKey) + if err != nil { + p.logger.Warn("read instance state", "err", err) + return + } + if found && string(value) == "1" { + p.desired = true + p.connected = true + if info, err := host.GetHostInfo(ctx); err == nil { + p.hostInfo = info + } + } +} + +func (p *provider) persistLocked(ctx context.Context, host *runtimehost.Client) { + if host == nil { + return + } + value := []byte("0") + if p.desired { + value = []byte("1") + } + if err := host.WriteInstanceState(ctx, desiredKey, value); err != nil { + p.logger.Warn("write instance state", "err", err) + } +} + +func (p *provider) reportLocked(ctx context.Context, host *runtimehost.Client, status *pluginv1.NetworkAccessStatus) { + if host == nil { + return + } + if err := host.ReportNetworkAccessStatus(ctx, status); err != nil { + p.logger.Warn("report network access status", "err", err) + } +} + +func (p *provider) statusLocked() *pluginv1.NetworkAccessStatus { + status := &pluginv1.NetworkAccessStatus{ + State: runtimehost.NetworkAccessStateDisconnected, + ProviderVersion: "stub " + version, + DesiredConnected: p.desired, + } + if !p.connected { + return status + } + status.State = runtimehost.NetworkAccessStateConnected + status.Hostname = "stub.invalid" + status.Addresses = []string{"100.64.0.1"} + if p.hostInfo != nil { + for _, l := range p.hostInfo.Listeners { + origin := fmt.Sprintf("https://%s:%d", status.Hostname, l.DefaultPort) + status.Listeners = append(status.Listeners, &pluginv1.NetworkAccessListener{Name: l.Name, Origin: origin}) + if l.Name == runtimehost.ListenerAPI { + status.Origin = origin + } + } + } + if status.Origin == "" { + status.Origin = "https://" + status.Hostname + } + return status +} + +func main() { + logger := hclog.New(&hclog.LoggerOptions{Name: "hello-network-access", Output: os.Stderr}) + sdkruntime.ServeManifest(manifestJSON, version, sdkruntime.CapabilityServers{ + NetworkAccessProvider: &provider{logger: logger}, + }) +} diff --git a/examples/hello-network-access/manifest.json b/examples/hello-network-access/manifest.json new file mode 100644 index 0000000..0cf0301 --- /dev/null +++ b/examples/hello-network-access/manifest.json @@ -0,0 +1,18 @@ +{ + "plugin_id": "example.hello-network-access", + "version": "0.1.0", + "checksum": "__CHECKSUM__", + "silo_api_version": "v1", + "capabilities": [ + { + "type": "network_access_provider.v1", + "id": "stub", + "display_name": "Stub Overlay", + "description": "A stub network access provider with no real overlay network. Useful as a host test fixture.", + "network_access_provider": { + "provider": "stub", + "display_name": "Stub Overlay" + } + } + ] +} diff --git a/pkg/pluginproto/silo/plugin/v1/common.pb.go b/pkg/pluginproto/silo/plugin/v1/common.pb.go index 68289e8..267874d 100644 --- a/pkg/pluginproto/silo/plugin/v1/common.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/common.pb.go @@ -977,8 +977,12 @@ type CapabilityDescriptor struct { // of type "watch_sync_provider.v1". Keeping this typed lets the host build // connection and authorization UI without launching the plugin. WatchSyncProvider *WatchSyncProviderDescriptor `protobuf:"bytes,10,opt,name=watch_sync_provider,json=watchSyncProvider,proto3" json:"watch_sync_provider,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Typed network-access contract metadata. Only meaningful for capabilities + // of type "network_access_provider.v1". The host lists available overlay + // providers from this descriptor without launching the plugin. + NetworkAccessProvider *NetworkAccessProviderDescriptor `protobuf:"bytes,11,opt,name=network_access_provider,json=networkAccessProvider,proto3" json:"network_access_provider,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CapabilityDescriptor) Reset() { @@ -1081,6 +1085,13 @@ func (x *CapabilityDescriptor) GetWatchSyncProvider() *WatchSyncProviderDescript return nil } +func (x *CapabilityDescriptor) GetNetworkAccessProvider() *NetworkAccessProviderDescriptor { + if x != nil { + return x.NetworkAccessProvider + } + return nil +} + // PluginPresentation describes a plugin for server operators and links them to // its publisher, source, support, and release history. These values are // self-declared by the plugin and never determine catalog approval. @@ -1599,7 +1610,7 @@ var File_silo_plugin_v1_common_proto protoreflect.FileDescriptor const file_silo_plugin_v1_common_proto_rawDesc = "" + "\n" + - "\x1bsilo/plugin/v1/common.proto\x12\x0esilo.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a(silo/plugin/v1/watch_sync_provider.proto\"7\n" + + "\x1bsilo/plugin/v1/common.proto\x12\x0esilo.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a,silo/plugin/v1/network_access_provider.proto\x1a(silo/plugin/v1/watch_sync_provider.proto\"7\n" + "\x11SupportedPlatform\x12\x0e\n" + "\x02os\x18\x01 \x01(\tR\x02os\x12\x12\n" + "\x04arch\x18\x02 \x01(\tR\x04arch\"\xd9\x01\n" + @@ -1676,7 +1687,7 @@ const file_silo_plugin_v1_common_proto_rawDesc = "" + "\rPackagedAsset\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12!\n" + "\fcontent_type\x18\x02 \x01(\tR\vcontentType\x12\x1c\n" + - "\tintegrity\x18\x03 \x01(\tR\tintegrity\"\xb4\x03\n" + + "\tintegrity\x18\x03 \x01(\tR\tintegrity\"\x9d\x04\n" + "\x14CapabilityDescriptor\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x0e\n" + "\x02id\x18\x02 \x01(\tR\x02id\x12!\n" + @@ -1689,7 +1700,8 @@ const file_silo_plugin_v1_common_proto_rawDesc = "" + "auth_modes\x18\b \x03(\tR\tauthModes\x12\x19\n" + "\bicon_url\x18\t \x01(\tR\aiconUrl\x12[\n" + "\x13watch_sync_provider\x18\n" + - " \x01(\v2+.silo.plugin.v1.WatchSyncProviderDescriptorR\x11watchSyncProvider\"\xa2\x03\n" + + " \x01(\v2+.silo.plugin.v1.WatchSyncProviderDescriptorR\x11watchSyncProvider\x12g\n" + + "\x17network_access_provider\x18\v \x01(\v2/.silo.plugin.v1.NetworkAccessProviderDescriptorR\x15networkAccessProvider\"\xa2\x03\n" + "\x12PluginPresentation\x12!\n" + "\fdisplay_name\x18\x01 \x01(\tR\vdisplayName\x12\x18\n" + "\asummary\x18\x02 \x01(\tR\asummary\x121\n" + @@ -1759,30 +1771,31 @@ func file_silo_plugin_v1_common_proto_rawDescGZIP() []byte { var file_silo_plugin_v1_common_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_silo_plugin_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 20) var file_silo_plugin_v1_common_proto_goTypes = []any{ - (AdminFormControl)(0), // 0: silo.plugin.v1.AdminFormControl - (*SupportedPlatform)(nil), // 1: silo.plugin.v1.SupportedPlatform - (*ConfigSchema)(nil), // 2: silo.plugin.v1.ConfigSchema - (*AdminFormOption)(nil), // 3: silo.plugin.v1.AdminFormOption - (*AdminFormField)(nil), // 4: silo.plugin.v1.AdminFormField - (*AdminFormCondition)(nil), // 5: silo.plugin.v1.AdminFormCondition - (*AdminFormValidation)(nil), // 6: silo.plugin.v1.AdminFormValidation - (*AdminFormSection)(nil), // 7: silo.plugin.v1.AdminFormSection - (*AdminFormDescriptor)(nil), // 8: silo.plugin.v1.AdminFormDescriptor - (*ConfigEntry)(nil), // 9: silo.plugin.v1.ConfigEntry - (*HttpRouteDescriptor)(nil), // 10: silo.plugin.v1.HttpRouteDescriptor - (*PackagedAsset)(nil), // 11: silo.plugin.v1.PackagedAsset - (*CapabilityDescriptor)(nil), // 12: silo.plugin.v1.CapabilityDescriptor - (*PluginPresentation)(nil), // 13: silo.plugin.v1.PluginPresentation - (*PluginManifest)(nil), // 14: silo.plugin.v1.PluginManifest - (*GetManifestRequest)(nil), // 15: silo.plugin.v1.GetManifestRequest - (*GetManifestResponse)(nil), // 16: silo.plugin.v1.GetManifestResponse - (*ConfigureRequest)(nil), // 17: silo.plugin.v1.ConfigureRequest - (*ConfigureResponse)(nil), // 18: silo.plugin.v1.ConfigureResponse - (*BindHostBrokerRequest)(nil), // 19: silo.plugin.v1.BindHostBrokerRequest - (*BindHostBrokerResponse)(nil), // 20: silo.plugin.v1.BindHostBrokerResponse - (*structpb.Value)(nil), // 21: google.protobuf.Value - (*structpb.Struct)(nil), // 22: google.protobuf.Struct - (*WatchSyncProviderDescriptor)(nil), // 23: silo.plugin.v1.WatchSyncProviderDescriptor + (AdminFormControl)(0), // 0: silo.plugin.v1.AdminFormControl + (*SupportedPlatform)(nil), // 1: silo.plugin.v1.SupportedPlatform + (*ConfigSchema)(nil), // 2: silo.plugin.v1.ConfigSchema + (*AdminFormOption)(nil), // 3: silo.plugin.v1.AdminFormOption + (*AdminFormField)(nil), // 4: silo.plugin.v1.AdminFormField + (*AdminFormCondition)(nil), // 5: silo.plugin.v1.AdminFormCondition + (*AdminFormValidation)(nil), // 6: silo.plugin.v1.AdminFormValidation + (*AdminFormSection)(nil), // 7: silo.plugin.v1.AdminFormSection + (*AdminFormDescriptor)(nil), // 8: silo.plugin.v1.AdminFormDescriptor + (*ConfigEntry)(nil), // 9: silo.plugin.v1.ConfigEntry + (*HttpRouteDescriptor)(nil), // 10: silo.plugin.v1.HttpRouteDescriptor + (*PackagedAsset)(nil), // 11: silo.plugin.v1.PackagedAsset + (*CapabilityDescriptor)(nil), // 12: silo.plugin.v1.CapabilityDescriptor + (*PluginPresentation)(nil), // 13: silo.plugin.v1.PluginPresentation + (*PluginManifest)(nil), // 14: silo.plugin.v1.PluginManifest + (*GetManifestRequest)(nil), // 15: silo.plugin.v1.GetManifestRequest + (*GetManifestResponse)(nil), // 16: silo.plugin.v1.GetManifestResponse + (*ConfigureRequest)(nil), // 17: silo.plugin.v1.ConfigureRequest + (*ConfigureResponse)(nil), // 18: silo.plugin.v1.ConfigureResponse + (*BindHostBrokerRequest)(nil), // 19: silo.plugin.v1.BindHostBrokerRequest + (*BindHostBrokerResponse)(nil), // 20: silo.plugin.v1.BindHostBrokerResponse + (*structpb.Value)(nil), // 21: google.protobuf.Value + (*structpb.Struct)(nil), // 22: google.protobuf.Struct + (*WatchSyncProviderDescriptor)(nil), // 23: silo.plugin.v1.WatchSyncProviderDescriptor + (*NetworkAccessProviderDescriptor)(nil), // 24: silo.plugin.v1.NetworkAccessProviderDescriptor } var file_silo_plugin_v1_common_proto_depIdxs = []int32{ 8, // 0: silo.plugin.v1.ConfigSchema.admin_form:type_name -> silo.plugin.v1.AdminFormDescriptor @@ -1798,27 +1811,28 @@ var file_silo_plugin_v1_common_proto_depIdxs = []int32{ 2, // 10: silo.plugin.v1.CapabilityDescriptor.config_schema:type_name -> silo.plugin.v1.ConfigSchema 22, // 11: silo.plugin.v1.CapabilityDescriptor.metadata:type_name -> google.protobuf.Struct 23, // 12: silo.plugin.v1.CapabilityDescriptor.watch_sync_provider:type_name -> silo.plugin.v1.WatchSyncProviderDescriptor - 1, // 13: silo.plugin.v1.PluginManifest.supported_platforms:type_name -> silo.plugin.v1.SupportedPlatform - 12, // 14: silo.plugin.v1.PluginManifest.capabilities:type_name -> silo.plugin.v1.CapabilityDescriptor - 2, // 15: silo.plugin.v1.PluginManifest.global_config_schema:type_name -> silo.plugin.v1.ConfigSchema - 2, // 16: silo.plugin.v1.PluginManifest.user_config_schema:type_name -> silo.plugin.v1.ConfigSchema - 10, // 17: silo.plugin.v1.PluginManifest.http_routes:type_name -> silo.plugin.v1.HttpRouteDescriptor - 11, // 18: silo.plugin.v1.PluginManifest.assets:type_name -> silo.plugin.v1.PackagedAsset - 22, // 19: silo.plugin.v1.PluginManifest.metadata:type_name -> google.protobuf.Struct - 13, // 20: silo.plugin.v1.PluginManifest.presentation:type_name -> silo.plugin.v1.PluginPresentation - 14, // 21: silo.plugin.v1.GetManifestResponse.manifest:type_name -> silo.plugin.v1.PluginManifest - 9, // 22: silo.plugin.v1.ConfigureRequest.config:type_name -> silo.plugin.v1.ConfigEntry - 15, // 23: silo.plugin.v1.Runtime.GetManifest:input_type -> silo.plugin.v1.GetManifestRequest - 17, // 24: silo.plugin.v1.Runtime.Configure:input_type -> silo.plugin.v1.ConfigureRequest - 19, // 25: silo.plugin.v1.Runtime.BindHostBroker:input_type -> silo.plugin.v1.BindHostBrokerRequest - 16, // 26: silo.plugin.v1.Runtime.GetManifest:output_type -> silo.plugin.v1.GetManifestResponse - 18, // 27: silo.plugin.v1.Runtime.Configure:output_type -> silo.plugin.v1.ConfigureResponse - 20, // 28: silo.plugin.v1.Runtime.BindHostBroker:output_type -> silo.plugin.v1.BindHostBrokerResponse - 26, // [26:29] is the sub-list for method output_type - 23, // [23:26] is the sub-list for method input_type - 23, // [23:23] is the sub-list for extension type_name - 23, // [23:23] is the sub-list for extension extendee - 0, // [0:23] is the sub-list for field type_name + 24, // 13: silo.plugin.v1.CapabilityDescriptor.network_access_provider:type_name -> silo.plugin.v1.NetworkAccessProviderDescriptor + 1, // 14: silo.plugin.v1.PluginManifest.supported_platforms:type_name -> silo.plugin.v1.SupportedPlatform + 12, // 15: silo.plugin.v1.PluginManifest.capabilities:type_name -> silo.plugin.v1.CapabilityDescriptor + 2, // 16: silo.plugin.v1.PluginManifest.global_config_schema:type_name -> silo.plugin.v1.ConfigSchema + 2, // 17: silo.plugin.v1.PluginManifest.user_config_schema:type_name -> silo.plugin.v1.ConfigSchema + 10, // 18: silo.plugin.v1.PluginManifest.http_routes:type_name -> silo.plugin.v1.HttpRouteDescriptor + 11, // 19: silo.plugin.v1.PluginManifest.assets:type_name -> silo.plugin.v1.PackagedAsset + 22, // 20: silo.plugin.v1.PluginManifest.metadata:type_name -> google.protobuf.Struct + 13, // 21: silo.plugin.v1.PluginManifest.presentation:type_name -> silo.plugin.v1.PluginPresentation + 14, // 22: silo.plugin.v1.GetManifestResponse.manifest:type_name -> silo.plugin.v1.PluginManifest + 9, // 23: silo.plugin.v1.ConfigureRequest.config:type_name -> silo.plugin.v1.ConfigEntry + 15, // 24: silo.plugin.v1.Runtime.GetManifest:input_type -> silo.plugin.v1.GetManifestRequest + 17, // 25: silo.plugin.v1.Runtime.Configure:input_type -> silo.plugin.v1.ConfigureRequest + 19, // 26: silo.plugin.v1.Runtime.BindHostBroker:input_type -> silo.plugin.v1.BindHostBrokerRequest + 16, // 27: silo.plugin.v1.Runtime.GetManifest:output_type -> silo.plugin.v1.GetManifestResponse + 18, // 28: silo.plugin.v1.Runtime.Configure:output_type -> silo.plugin.v1.ConfigureResponse + 20, // 29: silo.plugin.v1.Runtime.BindHostBroker:output_type -> silo.plugin.v1.BindHostBrokerResponse + 27, // [27:30] is the sub-list for method output_type + 24, // [24:27] is the sub-list for method input_type + 24, // [24:24] is the sub-list for extension type_name + 24, // [24:24] is the sub-list for extension extendee + 0, // [0:24] is the sub-list for field type_name } func init() { file_silo_plugin_v1_common_proto_init() } @@ -1826,6 +1840,7 @@ func file_silo_plugin_v1_common_proto_init() { if File_silo_plugin_v1_common_proto != nil { return } + file_silo_plugin_v1_network_access_provider_proto_init() file_silo_plugin_v1_watch_sync_provider_proto_init() type x struct{} out := protoimpl.TypeBuilder{ diff --git a/pkg/pluginproto/silo/plugin/v1/network_access_provider.pb.go b/pkg/pluginproto/silo/plugin/v1/network_access_provider.pb.go new file mode 100644 index 0000000..b3e6455 --- /dev/null +++ b/pkg/pluginproto/silo/plugin/v1/network_access_provider.pb.go @@ -0,0 +1,454 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: silo/plugin/v1/network_access_provider.proto + +package pluginv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type NetworkAccessConnectRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkAccessConnectRequest) Reset() { + *x = NetworkAccessConnectRequest{} + mi := &file_silo_plugin_v1_network_access_provider_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkAccessConnectRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkAccessConnectRequest) ProtoMessage() {} + +func (x *NetworkAccessConnectRequest) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_network_access_provider_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkAccessConnectRequest.ProtoReflect.Descriptor instead. +func (*NetworkAccessConnectRequest) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_network_access_provider_proto_rawDescGZIP(), []int{0} +} + +type NetworkAccessDisconnectRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkAccessDisconnectRequest) Reset() { + *x = NetworkAccessDisconnectRequest{} + mi := &file_silo_plugin_v1_network_access_provider_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkAccessDisconnectRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkAccessDisconnectRequest) ProtoMessage() {} + +func (x *NetworkAccessDisconnectRequest) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_network_access_provider_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkAccessDisconnectRequest.ProtoReflect.Descriptor instead. +func (*NetworkAccessDisconnectRequest) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_network_access_provider_proto_rawDescGZIP(), []int{1} +} + +type NetworkAccessGetStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkAccessGetStatusRequest) Reset() { + *x = NetworkAccessGetStatusRequest{} + mi := &file_silo_plugin_v1_network_access_provider_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkAccessGetStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkAccessGetStatusRequest) ProtoMessage() {} + +func (x *NetworkAccessGetStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_network_access_provider_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkAccessGetStatusRequest.ProtoReflect.Descriptor instead. +func (*NetworkAccessGetStatusRequest) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_network_access_provider_proto_rawDescGZIP(), []int{2} +} + +// NetworkAccessStatus is the provider's view of one plugin instance. Every +// host (API server, each proxy node) runs its own instance and reports its own +// status; the host aggregates them. +type NetworkAccessStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + // One of: disconnected | awaiting_authorization | connecting | connected | error. + State string `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` + // Overlay DNS name, e.g. silo.tail1234.ts.net. + Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` + // scheme://host[:port] clients should use for the api listener. + Origin string `protobuf:"bytes,3,opt,name=origin,proto3" json:"origin,omitempty"` + // Overlay IP addresses assigned to this instance. + Addresses []string `protobuf:"bytes,4,rep,name=addresses,proto3" json:"addresses,omitempty"` + // One entry per exposed host listener (see RuntimeHost.GetHostInfo). + Listeners []*NetworkAccessListener `protobuf:"bytes,5,rep,name=listeners,proto3" json:"listeners,omitempty"` + // Interactive enrollment URL. Only set while state is awaiting_authorization. + // Admin-only; the plugin must never log it. + AuthUrl string `protobuf:"bytes,6,opt,name=auth_url,json=authUrl,proto3" json:"auth_url,omitempty"` + // Human-readable failure detail when state is error. + Error string `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"` + // Provider client version shown to admins, e.g. "tsnet 1.102.4". + ProviderVersion string `protobuf:"bytes,8,opt,name=provider_version,json=providerVersion,proto3" json:"provider_version,omitempty"` + // Plugin-owned intent that survives restarts. The plugin persists it in + // instance state and reconnects on start when true. + DesiredConnected bool `protobuf:"varint,9,opt,name=desired_connected,json=desiredConnected,proto3" json:"desired_connected,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkAccessStatus) Reset() { + *x = NetworkAccessStatus{} + mi := &file_silo_plugin_v1_network_access_provider_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkAccessStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkAccessStatus) ProtoMessage() {} + +func (x *NetworkAccessStatus) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_network_access_provider_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkAccessStatus.ProtoReflect.Descriptor instead. +func (*NetworkAccessStatus) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_network_access_provider_proto_rawDescGZIP(), []int{3} +} + +func (x *NetworkAccessStatus) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *NetworkAccessStatus) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *NetworkAccessStatus) GetOrigin() string { + if x != nil { + return x.Origin + } + return "" +} + +func (x *NetworkAccessStatus) GetAddresses() []string { + if x != nil { + return x.Addresses + } + return nil +} + +func (x *NetworkAccessStatus) GetListeners() []*NetworkAccessListener { + if x != nil { + return x.Listeners + } + return nil +} + +func (x *NetworkAccessStatus) GetAuthUrl() string { + if x != nil { + return x.AuthUrl + } + return "" +} + +func (x *NetworkAccessStatus) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *NetworkAccessStatus) GetProviderVersion() string { + if x != nil { + return x.ProviderVersion + } + return "" +} + +func (x *NetworkAccessStatus) GetDesiredConnected() bool { + if x != nil { + return x.DesiredConnected + } + return false +} + +// NetworkAccessListener maps one host listener onto its overlay origin. +type NetworkAccessListener struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Listener name from RuntimeHost.GetHostInfo: api | jellyfin | abs. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Overlay origin serving that listener, scheme://host[:port]. + Origin string `protobuf:"bytes,2,opt,name=origin,proto3" json:"origin,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkAccessListener) Reset() { + *x = NetworkAccessListener{} + mi := &file_silo_plugin_v1_network_access_provider_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkAccessListener) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkAccessListener) ProtoMessage() {} + +func (x *NetworkAccessListener) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_network_access_provider_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkAccessListener.ProtoReflect.Descriptor instead. +func (*NetworkAccessListener) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_network_access_provider_proto_rawDescGZIP(), []int{4} +} + +func (x *NetworkAccessListener) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *NetworkAccessListener) GetOrigin() string { + if x != nil { + return x.Origin + } + return "" +} + +// NetworkAccessProviderDescriptor is the typed manifest metadata for a +// network_access_provider.v1 capability. The host lists providers from it +// without launching the plugin. +type NetworkAccessProviderDescriptor struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stable provider slug, e.g. "tailscale", "netbird". Lowercase, path-safe. + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + // Operator-facing name, e.g. "Tailscale". + DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkAccessProviderDescriptor) Reset() { + *x = NetworkAccessProviderDescriptor{} + mi := &file_silo_plugin_v1_network_access_provider_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkAccessProviderDescriptor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkAccessProviderDescriptor) ProtoMessage() {} + +func (x *NetworkAccessProviderDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_network_access_provider_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkAccessProviderDescriptor.ProtoReflect.Descriptor instead. +func (*NetworkAccessProviderDescriptor) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_network_access_provider_proto_rawDescGZIP(), []int{5} +} + +func (x *NetworkAccessProviderDescriptor) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *NetworkAccessProviderDescriptor) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +var File_silo_plugin_v1_network_access_provider_proto protoreflect.FileDescriptor + +const file_silo_plugin_v1_network_access_provider_proto_rawDesc = "" + + "\n" + + ",silo/plugin/v1/network_access_provider.proto\x12\x0esilo.plugin.v1\"\x1d\n" + + "\x1bNetworkAccessConnectRequest\" \n" + + "\x1eNetworkAccessDisconnectRequest\"\x1f\n" + + "\x1dNetworkAccessGetStatusRequest\"\xcb\x02\n" + + "\x13NetworkAccessStatus\x12\x14\n" + + "\x05state\x18\x01 \x01(\tR\x05state\x12\x1a\n" + + "\bhostname\x18\x02 \x01(\tR\bhostname\x12\x16\n" + + "\x06origin\x18\x03 \x01(\tR\x06origin\x12\x1c\n" + + "\taddresses\x18\x04 \x03(\tR\taddresses\x12C\n" + + "\tlisteners\x18\x05 \x03(\v2%.silo.plugin.v1.NetworkAccessListenerR\tlisteners\x12\x19\n" + + "\bauth_url\x18\x06 \x01(\tR\aauthUrl\x12\x14\n" + + "\x05error\x18\a \x01(\tR\x05error\x12)\n" + + "\x10provider_version\x18\b \x01(\tR\x0fproviderVersion\x12+\n" + + "\x11desired_connected\x18\t \x01(\bR\x10desiredConnected\"C\n" + + "\x15NetworkAccessListener\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" + + "\x06origin\x18\x02 \x01(\tR\x06origin\"`\n" + + "\x1fNetworkAccessProviderDescriptor\x12\x1a\n" + + "\bprovider\x18\x01 \x01(\tR\bprovider\x12!\n" + + "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName2\xb8\x02\n" + + "\x15NetworkAccessProvider\x12[\n" + + "\aConnect\x12+.silo.plugin.v1.NetworkAccessConnectRequest\x1a#.silo.plugin.v1.NetworkAccessStatus\x12a\n" + + "\n" + + "Disconnect\x12..silo.plugin.v1.NetworkAccessDisconnectRequest\x1a#.silo.plugin.v1.NetworkAccessStatus\x12_\n" + + "\tGetStatus\x12-.silo.plugin.v1.NetworkAccessGetStatusRequest\x1a#.silo.plugin.v1.NetworkAccessStatusBPZNgithub.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1;pluginv1b\x06proto3" + +var ( + file_silo_plugin_v1_network_access_provider_proto_rawDescOnce sync.Once + file_silo_plugin_v1_network_access_provider_proto_rawDescData []byte +) + +func file_silo_plugin_v1_network_access_provider_proto_rawDescGZIP() []byte { + file_silo_plugin_v1_network_access_provider_proto_rawDescOnce.Do(func() { + file_silo_plugin_v1_network_access_provider_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_silo_plugin_v1_network_access_provider_proto_rawDesc), len(file_silo_plugin_v1_network_access_provider_proto_rawDesc))) + }) + return file_silo_plugin_v1_network_access_provider_proto_rawDescData +} + +var file_silo_plugin_v1_network_access_provider_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_silo_plugin_v1_network_access_provider_proto_goTypes = []any{ + (*NetworkAccessConnectRequest)(nil), // 0: silo.plugin.v1.NetworkAccessConnectRequest + (*NetworkAccessDisconnectRequest)(nil), // 1: silo.plugin.v1.NetworkAccessDisconnectRequest + (*NetworkAccessGetStatusRequest)(nil), // 2: silo.plugin.v1.NetworkAccessGetStatusRequest + (*NetworkAccessStatus)(nil), // 3: silo.plugin.v1.NetworkAccessStatus + (*NetworkAccessListener)(nil), // 4: silo.plugin.v1.NetworkAccessListener + (*NetworkAccessProviderDescriptor)(nil), // 5: silo.plugin.v1.NetworkAccessProviderDescriptor +} +var file_silo_plugin_v1_network_access_provider_proto_depIdxs = []int32{ + 4, // 0: silo.plugin.v1.NetworkAccessStatus.listeners:type_name -> silo.plugin.v1.NetworkAccessListener + 0, // 1: silo.plugin.v1.NetworkAccessProvider.Connect:input_type -> silo.plugin.v1.NetworkAccessConnectRequest + 1, // 2: silo.plugin.v1.NetworkAccessProvider.Disconnect:input_type -> silo.plugin.v1.NetworkAccessDisconnectRequest + 2, // 3: silo.plugin.v1.NetworkAccessProvider.GetStatus:input_type -> silo.plugin.v1.NetworkAccessGetStatusRequest + 3, // 4: silo.plugin.v1.NetworkAccessProvider.Connect:output_type -> silo.plugin.v1.NetworkAccessStatus + 3, // 5: silo.plugin.v1.NetworkAccessProvider.Disconnect:output_type -> silo.plugin.v1.NetworkAccessStatus + 3, // 6: silo.plugin.v1.NetworkAccessProvider.GetStatus:output_type -> silo.plugin.v1.NetworkAccessStatus + 4, // [4:7] is the sub-list for method output_type + 1, // [1:4] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_silo_plugin_v1_network_access_provider_proto_init() } +func file_silo_plugin_v1_network_access_provider_proto_init() { + if File_silo_plugin_v1_network_access_provider_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_silo_plugin_v1_network_access_provider_proto_rawDesc), len(file_silo_plugin_v1_network_access_provider_proto_rawDesc)), + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_silo_plugin_v1_network_access_provider_proto_goTypes, + DependencyIndexes: file_silo_plugin_v1_network_access_provider_proto_depIdxs, + MessageInfos: file_silo_plugin_v1_network_access_provider_proto_msgTypes, + }.Build() + File_silo_plugin_v1_network_access_provider_proto = out.File + file_silo_plugin_v1_network_access_provider_proto_goTypes = nil + file_silo_plugin_v1_network_access_provider_proto_depIdxs = nil +} diff --git a/pkg/pluginproto/silo/plugin/v1/network_access_provider_grpc.pb.go b/pkg/pluginproto/silo/plugin/v1/network_access_provider_grpc.pb.go new file mode 100644 index 0000000..2a4bb8b --- /dev/null +++ b/pkg/pluginproto/silo/plugin/v1/network_access_provider_grpc.pb.go @@ -0,0 +1,219 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: silo/plugin/v1/network_access_provider.proto + +package pluginv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + NetworkAccessProvider_Connect_FullMethodName = "/silo.plugin.v1.NetworkAccessProvider/Connect" + NetworkAccessProvider_Disconnect_FullMethodName = "/silo.plugin.v1.NetworkAccessProvider/Disconnect" + NetworkAccessProvider_GetStatus_FullMethodName = "/silo.plugin.v1.NetworkAccessProvider/GetStatus" +) + +// NetworkAccessProviderClient is the client API for NetworkAccessProvider service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// NetworkAccessProvider gives a Silo deployment an overlay-network identity +// (Tailscale, NetBird, ...). The plugin owns the overlay client and a reverse +// proxy in front of the host listeners it learns from RuntimeHost.GetHostInfo; +// the host owns supervision, per-instance state storage, status aggregation, +// and the admin API. Plugins declaring this capability are resident: the host +// starts them at boot, restarts them on crash, and stops them last. +type NetworkAccessProviderClient interface { + // Connect brings the overlay identity up and starts proxying. It returns the + // status reached so far; enrollment may continue in the background and be + // reported through RuntimeHost.ReportNetworkAccessStatus. + Connect(ctx context.Context, in *NetworkAccessConnectRequest, opts ...grpc.CallOption) (*NetworkAccessStatus, error) + // Disconnect tears the overlay listener down and clears desired_connected. + Disconnect(ctx context.Context, in *NetworkAccessDisconnectRequest, opts ...grpc.CallOption) (*NetworkAccessStatus, error) + // GetStatus returns the current status without changing it. + GetStatus(ctx context.Context, in *NetworkAccessGetStatusRequest, opts ...grpc.CallOption) (*NetworkAccessStatus, error) +} + +type networkAccessProviderClient struct { + cc grpc.ClientConnInterface +} + +func NewNetworkAccessProviderClient(cc grpc.ClientConnInterface) NetworkAccessProviderClient { + return &networkAccessProviderClient{cc} +} + +func (c *networkAccessProviderClient) Connect(ctx context.Context, in *NetworkAccessConnectRequest, opts ...grpc.CallOption) (*NetworkAccessStatus, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(NetworkAccessStatus) + err := c.cc.Invoke(ctx, NetworkAccessProvider_Connect_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *networkAccessProviderClient) Disconnect(ctx context.Context, in *NetworkAccessDisconnectRequest, opts ...grpc.CallOption) (*NetworkAccessStatus, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(NetworkAccessStatus) + err := c.cc.Invoke(ctx, NetworkAccessProvider_Disconnect_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *networkAccessProviderClient) GetStatus(ctx context.Context, in *NetworkAccessGetStatusRequest, opts ...grpc.CallOption) (*NetworkAccessStatus, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(NetworkAccessStatus) + err := c.cc.Invoke(ctx, NetworkAccessProvider_GetStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// NetworkAccessProviderServer is the server API for NetworkAccessProvider service. +// All implementations should embed UnimplementedNetworkAccessProviderServer +// for forward compatibility. +// +// NetworkAccessProvider gives a Silo deployment an overlay-network identity +// (Tailscale, NetBird, ...). The plugin owns the overlay client and a reverse +// proxy in front of the host listeners it learns from RuntimeHost.GetHostInfo; +// the host owns supervision, per-instance state storage, status aggregation, +// and the admin API. Plugins declaring this capability are resident: the host +// starts them at boot, restarts them on crash, and stops them last. +type NetworkAccessProviderServer interface { + // Connect brings the overlay identity up and starts proxying. It returns the + // status reached so far; enrollment may continue in the background and be + // reported through RuntimeHost.ReportNetworkAccessStatus. + Connect(context.Context, *NetworkAccessConnectRequest) (*NetworkAccessStatus, error) + // Disconnect tears the overlay listener down and clears desired_connected. + Disconnect(context.Context, *NetworkAccessDisconnectRequest) (*NetworkAccessStatus, error) + // GetStatus returns the current status without changing it. + GetStatus(context.Context, *NetworkAccessGetStatusRequest) (*NetworkAccessStatus, error) +} + +// UnimplementedNetworkAccessProviderServer should be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedNetworkAccessProviderServer struct{} + +func (UnimplementedNetworkAccessProviderServer) Connect(context.Context, *NetworkAccessConnectRequest) (*NetworkAccessStatus, error) { + return nil, status.Error(codes.Unimplemented, "method Connect not implemented") +} +func (UnimplementedNetworkAccessProviderServer) Disconnect(context.Context, *NetworkAccessDisconnectRequest) (*NetworkAccessStatus, error) { + return nil, status.Error(codes.Unimplemented, "method Disconnect not implemented") +} +func (UnimplementedNetworkAccessProviderServer) GetStatus(context.Context, *NetworkAccessGetStatusRequest) (*NetworkAccessStatus, error) { + return nil, status.Error(codes.Unimplemented, "method GetStatus not implemented") +} +func (UnimplementedNetworkAccessProviderServer) testEmbeddedByValue() {} + +// UnsafeNetworkAccessProviderServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to NetworkAccessProviderServer will +// result in compilation errors. +type UnsafeNetworkAccessProviderServer interface { + mustEmbedUnimplementedNetworkAccessProviderServer() +} + +func RegisterNetworkAccessProviderServer(s grpc.ServiceRegistrar, srv NetworkAccessProviderServer) { + // If the following call panics, it indicates UnimplementedNetworkAccessProviderServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&NetworkAccessProvider_ServiceDesc, srv) +} + +func _NetworkAccessProvider_Connect_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NetworkAccessConnectRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NetworkAccessProviderServer).Connect(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NetworkAccessProvider_Connect_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NetworkAccessProviderServer).Connect(ctx, req.(*NetworkAccessConnectRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NetworkAccessProvider_Disconnect_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NetworkAccessDisconnectRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NetworkAccessProviderServer).Disconnect(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NetworkAccessProvider_Disconnect_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NetworkAccessProviderServer).Disconnect(ctx, req.(*NetworkAccessDisconnectRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NetworkAccessProvider_GetStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NetworkAccessGetStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NetworkAccessProviderServer).GetStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NetworkAccessProvider_GetStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NetworkAccessProviderServer).GetStatus(ctx, req.(*NetworkAccessGetStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// NetworkAccessProvider_ServiceDesc is the grpc.ServiceDesc for NetworkAccessProvider service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var NetworkAccessProvider_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "silo.plugin.v1.NetworkAccessProvider", + HandlerType: (*NetworkAccessProviderServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Connect", + Handler: _NetworkAccessProvider_Connect_Handler, + }, + { + MethodName: "Disconnect", + Handler: _NetworkAccessProvider_Disconnect_Handler, + }, + { + MethodName: "GetStatus", + Handler: _NetworkAccessProvider_GetStatus_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "silo/plugin/v1/network_access_provider.proto", +} diff --git a/pkg/pluginproto/silo/plugin/v1/runtime_host.pb.go b/pkg/pluginproto/silo/plugin/v1/runtime_host.pb.go index a7643a5..8569828 100644 --- a/pkg/pluginproto/silo/plugin/v1/runtime_host.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/runtime_host.pb.go @@ -350,8 +350,23 @@ type GetHostInfoResponse struct { PublicBaseUrl string `protobuf:"bytes,1,opt,name=public_base_url,json=publicBaseUrl,proto3" json:"public_base_url,omitempty"` InternalBaseUrl string `protobuf:"bytes,2,opt,name=internal_base_url,json=internalBaseUrl,proto3" json:"internal_base_url,omitempty"` PluginProxyBaseUrl string `protobuf:"bytes,3,opt,name=plugin_proxy_base_url,json=pluginProxyBaseUrl,proto3" json:"plugin_proxy_base_url,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Role of the process hosting this plugin instance: api | proxy. + HostRole string `protobuf:"bytes,4,opt,name=host_role,json=hostRole,proto3" json:"host_role,omitempty"` + // Node name on proxies, or the server name on the api host. + HostName string `protobuf:"bytes,5,opt,name=host_name,json=hostName,proto3" json:"host_name,omitempty"` + // stream_nodes.id on proxies; 0 on the api host. + NodeId int64 `protobuf:"varint,6,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + // Per-process-start secret that a network access provider stamps on + // proxied requests as X-Silo-Ingress-Token. Treat as a credential: keep it + // in memory, never log or persist it, and re-read GetHostInfo after a + // restart because the host rotates it on every start. + IngressToken string `protobuf:"bytes,7,opt,name=ingress_token,json=ingressToken,proto3" json:"ingress_token,omitempty"` + // Host listeners a network access provider should expose. Always contains + // "api"; "jellyfin" and "abs" appear when those listeners are enabled. + // Proxies expose only "api". + Listeners []*HostListener `protobuf:"bytes,8,rep,name=listeners,proto3" json:"listeners,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetHostInfoResponse) Reset() { @@ -405,6 +420,107 @@ func (x *GetHostInfoResponse) GetPluginProxyBaseUrl() string { return "" } +func (x *GetHostInfoResponse) GetHostRole() string { + if x != nil { + return x.HostRole + } + return "" +} + +func (x *GetHostInfoResponse) GetHostName() string { + if x != nil { + return x.HostName + } + return "" +} + +func (x *GetHostInfoResponse) GetNodeId() int64 { + if x != nil { + return x.NodeId + } + return 0 +} + +func (x *GetHostInfoResponse) GetIngressToken() string { + if x != nil { + return x.IngressToken + } + return "" +} + +func (x *GetHostInfoResponse) GetListeners() []*HostListener { + if x != nil { + return x.Listeners + } + return nil +} + +// HostListener is one local listener the host asks a network access provider +// to expose on the overlay network. +type HostListener struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Listener name: api | jellyfin | abs. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Loopback dial address for the listener, host:port. + Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` + // Port the plugin should expose this listener on (e.g. 443 for api under + // HTTPS, 8096 for jellyfin, 13378 for abs). Zero means provider default. + DefaultPort int32 `protobuf:"varint,3,opt,name=default_port,json=defaultPort,proto3" json:"default_port,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HostListener) Reset() { + *x = HostListener{} + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HostListener) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HostListener) ProtoMessage() {} + +func (x *HostListener) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HostListener.ProtoReflect.Descriptor instead. +func (*HostListener) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{8} +} + +func (x *HostListener) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *HostListener) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *HostListener) GetDefaultPort() int32 { + if x != nil { + return x.DefaultPort + } + return 0 +} + type ListLibrariesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Optional: scope results to libraries this user is allowed to see. @@ -415,7 +531,7 @@ type ListLibrariesRequest struct { func (x *ListLibrariesRequest) Reset() { *x = ListLibrariesRequest{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[8] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -427,7 +543,7 @@ func (x *ListLibrariesRequest) String() string { func (*ListLibrariesRequest) ProtoMessage() {} func (x *ListLibrariesRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[8] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -440,7 +556,7 @@ func (x *ListLibrariesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLibrariesRequest.ProtoReflect.Descriptor instead. func (*ListLibrariesRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{8} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{9} } func (x *ListLibrariesRequest) GetUserId() string { @@ -459,7 +575,7 @@ type ListLibrariesResponse struct { func (x *ListLibrariesResponse) Reset() { *x = ListLibrariesResponse{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[9] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -471,7 +587,7 @@ func (x *ListLibrariesResponse) String() string { func (*ListLibrariesResponse) ProtoMessage() {} func (x *ListLibrariesResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[9] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -484,7 +600,7 @@ func (x *ListLibrariesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLibrariesResponse.ProtoReflect.Descriptor instead. func (*ListLibrariesResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{9} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{10} } func (x *ListLibrariesResponse) GetLibraries() []*Library { @@ -506,7 +622,7 @@ type Library struct { func (x *Library) Reset() { *x = Library{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[10] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -518,7 +634,7 @@ func (x *Library) String() string { func (*Library) ProtoMessage() {} func (x *Library) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[10] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -531,7 +647,7 @@ func (x *Library) ProtoReflect() protoreflect.Message { // Deprecated: Use Library.ProtoReflect.Descriptor instead. func (*Library) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{10} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{11} } func (x *Library) GetId() string { @@ -570,7 +686,7 @@ type CheckMediaPresenceRequest struct { func (x *CheckMediaPresenceRequest) Reset() { *x = CheckMediaPresenceRequest{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[11] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -582,7 +698,7 @@ func (x *CheckMediaPresenceRequest) String() string { func (*CheckMediaPresenceRequest) ProtoMessage() {} func (x *CheckMediaPresenceRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[11] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -595,7 +711,7 @@ func (x *CheckMediaPresenceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckMediaPresenceRequest.ProtoReflect.Descriptor instead. func (*CheckMediaPresenceRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{11} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{12} } func (x *CheckMediaPresenceRequest) GetProvider() string { @@ -630,7 +746,7 @@ type CheckMediaPresenceResponse struct { func (x *CheckMediaPresenceResponse) Reset() { *x = CheckMediaPresenceResponse{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[12] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -642,7 +758,7 @@ func (x *CheckMediaPresenceResponse) String() string { func (*CheckMediaPresenceResponse) ProtoMessage() {} func (x *CheckMediaPresenceResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[12] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -655,7 +771,7 @@ func (x *CheckMediaPresenceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckMediaPresenceResponse.ProtoReflect.Descriptor instead. func (*CheckMediaPresenceResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{12} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{13} } func (x *CheckMediaPresenceResponse) GetPresent() []*MediaPresence { @@ -677,7 +793,7 @@ type MediaPresence struct { func (x *MediaPresence) Reset() { *x = MediaPresence{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[13] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -689,7 +805,7 @@ func (x *MediaPresence) String() string { func (*MediaPresence) ProtoMessage() {} func (x *MediaPresence) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[13] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -702,7 +818,7 @@ func (x *MediaPresence) ProtoReflect() protoreflect.Message { // Deprecated: Use MediaPresence.ProtoReflect.Descriptor instead. func (*MediaPresence) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{13} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{14} } func (x *MediaPresence) GetExternalId() string { @@ -741,7 +857,7 @@ type ListInstalledPluginsRequest struct { func (x *ListInstalledPluginsRequest) Reset() { *x = ListInstalledPluginsRequest{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[14] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -753,7 +869,7 @@ func (x *ListInstalledPluginsRequest) String() string { func (*ListInstalledPluginsRequest) ProtoMessage() {} func (x *ListInstalledPluginsRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[14] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -766,7 +882,7 @@ func (x *ListInstalledPluginsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListInstalledPluginsRequest.ProtoReflect.Descriptor instead. func (*ListInstalledPluginsRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{14} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{15} } type ListInstalledPluginsResponse struct { @@ -778,7 +894,7 @@ type ListInstalledPluginsResponse struct { func (x *ListInstalledPluginsResponse) Reset() { *x = ListInstalledPluginsResponse{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[15] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -790,7 +906,7 @@ func (x *ListInstalledPluginsResponse) String() string { func (*ListInstalledPluginsResponse) ProtoMessage() {} func (x *ListInstalledPluginsResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[15] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -803,7 +919,7 @@ func (x *ListInstalledPluginsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListInstalledPluginsResponse.ProtoReflect.Descriptor instead. func (*ListInstalledPluginsResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{15} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{16} } func (x *ListInstalledPluginsResponse) GetPlugins() []*InstalledPlugin { @@ -826,7 +942,7 @@ type InstalledPlugin struct { func (x *InstalledPlugin) Reset() { *x = InstalledPlugin{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[16] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -838,7 +954,7 @@ func (x *InstalledPlugin) String() string { func (*InstalledPlugin) ProtoMessage() {} func (x *InstalledPlugin) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[16] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -851,7 +967,7 @@ func (x *InstalledPlugin) ProtoReflect() protoreflect.Message { // Deprecated: Use InstalledPlugin.ProtoReflect.Descriptor instead. func (*InstalledPlugin) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{16} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{17} } func (x *InstalledPlugin) GetInstallationId() int64 { @@ -899,7 +1015,7 @@ type SetGlobalConfigEntryRequest struct { func (x *SetGlobalConfigEntryRequest) Reset() { *x = SetGlobalConfigEntryRequest{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[17] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -911,7 +1027,7 @@ func (x *SetGlobalConfigEntryRequest) String() string { func (*SetGlobalConfigEntryRequest) ProtoMessage() {} func (x *SetGlobalConfigEntryRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[17] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -924,7 +1040,7 @@ func (x *SetGlobalConfigEntryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetGlobalConfigEntryRequest.ProtoReflect.Descriptor instead. func (*SetGlobalConfigEntryRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{17} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{18} } func (x *SetGlobalConfigEntryRequest) GetKey() string { @@ -949,7 +1065,7 @@ type SetGlobalConfigEntryResponse struct { func (x *SetGlobalConfigEntryResponse) Reset() { *x = SetGlobalConfigEntryResponse{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[18] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -961,7 +1077,7 @@ func (x *SetGlobalConfigEntryResponse) String() string { func (*SetGlobalConfigEntryResponse) ProtoMessage() {} func (x *SetGlobalConfigEntryResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[18] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -974,7 +1090,7 @@ func (x *SetGlobalConfigEntryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetGlobalConfigEntryResponse.ProtoReflect.Descriptor instead. func (*SetGlobalConfigEntryResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{18} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{19} } type ListLibraryMediaRequest struct { @@ -998,7 +1114,7 @@ type ListLibraryMediaRequest struct { func (x *ListLibraryMediaRequest) Reset() { *x = ListLibraryMediaRequest{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[19] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1010,7 +1126,7 @@ func (x *ListLibraryMediaRequest) String() string { func (*ListLibraryMediaRequest) ProtoMessage() {} func (x *ListLibraryMediaRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[19] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1023,7 +1139,7 @@ func (x *ListLibraryMediaRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLibraryMediaRequest.ProtoReflect.Descriptor instead. func (*ListLibraryMediaRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{19} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{20} } func (x *ListLibraryMediaRequest) GetLibraryIds() []string { @@ -1107,7 +1223,7 @@ type ListLibraryMediaResponse struct { func (x *ListLibraryMediaResponse) Reset() { *x = ListLibraryMediaResponse{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[20] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1119,7 +1235,7 @@ func (x *ListLibraryMediaResponse) String() string { func (*ListLibraryMediaResponse) ProtoMessage() {} func (x *ListLibraryMediaResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[20] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1132,7 +1248,7 @@ func (x *ListLibraryMediaResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLibraryMediaResponse.ProtoReflect.Descriptor instead. func (*ListLibraryMediaResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{20} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{21} } func (x *ListLibraryMediaResponse) GetItems() []*CatalogMediaItem { @@ -1179,7 +1295,7 @@ type CatalogMediaItem struct { func (x *CatalogMediaItem) Reset() { *x = CatalogMediaItem{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[21] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1191,7 +1307,7 @@ func (x *CatalogMediaItem) String() string { func (*CatalogMediaItem) ProtoMessage() {} func (x *CatalogMediaItem) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[21] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1204,7 +1320,7 @@ func (x *CatalogMediaItem) ProtoReflect() protoreflect.Message { // Deprecated: Use CatalogMediaItem.ProtoReflect.Descriptor instead. func (*CatalogMediaItem) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{21} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{22} } func (x *CatalogMediaItem) GetMediaId() string { @@ -1322,7 +1438,7 @@ type GetCatalogStatsRequest struct { func (x *GetCatalogStatsRequest) Reset() { *x = GetCatalogStatsRequest{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[22] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1334,7 +1450,7 @@ func (x *GetCatalogStatsRequest) String() string { func (*GetCatalogStatsRequest) ProtoMessage() {} func (x *GetCatalogStatsRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[22] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1347,7 +1463,7 @@ func (x *GetCatalogStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCatalogStatsRequest.ProtoReflect.Descriptor instead. func (*GetCatalogStatsRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{22} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{23} } func (x *GetCatalogStatsRequest) GetLibraryIds() []string { @@ -1368,7 +1484,7 @@ type GetCatalogStatsResponse struct { func (x *GetCatalogStatsResponse) Reset() { *x = GetCatalogStatsResponse{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[23] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1380,7 +1496,7 @@ func (x *GetCatalogStatsResponse) String() string { func (*GetCatalogStatsResponse) ProtoMessage() {} func (x *GetCatalogStatsResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[23] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1393,7 +1509,7 @@ func (x *GetCatalogStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCatalogStatsResponse.ProtoReflect.Descriptor instead. func (*GetCatalogStatsResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{23} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{24} } func (x *GetCatalogStatsResponse) GetTotalItems() int32 { @@ -1427,7 +1543,7 @@ type CatalogTypeCount struct { func (x *CatalogTypeCount) Reset() { *x = CatalogTypeCount{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[24] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1439,7 +1555,7 @@ func (x *CatalogTypeCount) String() string { func (*CatalogTypeCount) ProtoMessage() {} func (x *CatalogTypeCount) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[24] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1452,7 +1568,7 @@ func (x *CatalogTypeCount) ProtoReflect() protoreflect.Message { // Deprecated: Use CatalogTypeCount.ProtoReflect.Descriptor instead. func (*CatalogTypeCount) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{24} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{25} } func (x *CatalogTypeCount) GetMediaType() string { @@ -1481,7 +1597,7 @@ type CatalogLibraryCount struct { func (x *CatalogLibraryCount) Reset() { *x = CatalogLibraryCount{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[25] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1493,7 +1609,7 @@ func (x *CatalogLibraryCount) String() string { func (*CatalogLibraryCount) ProtoMessage() {} func (x *CatalogLibraryCount) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[25] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1506,7 +1622,7 @@ func (x *CatalogLibraryCount) ProtoReflect() protoreflect.Message { // Deprecated: Use CatalogLibraryCount.ProtoReflect.Descriptor instead. func (*CatalogLibraryCount) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{25} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{26} } func (x *CatalogLibraryCount) GetLibraryId() string { @@ -1557,7 +1673,7 @@ type ResolveCatalogImageURLsRequest struct { func (x *ResolveCatalogImageURLsRequest) Reset() { *x = ResolveCatalogImageURLsRequest{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[26] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1569,7 +1685,7 @@ func (x *ResolveCatalogImageURLsRequest) String() string { func (*ResolveCatalogImageURLsRequest) ProtoMessage() {} func (x *ResolveCatalogImageURLsRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[26] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1582,7 +1698,7 @@ func (x *ResolveCatalogImageURLsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResolveCatalogImageURLsRequest.ProtoReflect.Descriptor instead. func (*ResolveCatalogImageURLsRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{26} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{27} } func (x *ResolveCatalogImageURLsRequest) GetPaths() []string { @@ -1611,7 +1727,7 @@ type ResolveCatalogImageURLsResponse struct { func (x *ResolveCatalogImageURLsResponse) Reset() { *x = ResolveCatalogImageURLsResponse{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[27] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1623,7 +1739,7 @@ func (x *ResolveCatalogImageURLsResponse) String() string { func (*ResolveCatalogImageURLsResponse) ProtoMessage() {} func (x *ResolveCatalogImageURLsResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[27] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1636,7 +1752,7 @@ func (x *ResolveCatalogImageURLsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResolveCatalogImageURLsResponse.ProtoReflect.Descriptor instead. func (*ResolveCatalogImageURLsResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{27} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{28} } func (x *ResolveCatalogImageURLsResponse) GetUrls() map[string]string { @@ -1666,7 +1782,7 @@ type MintScopedStreamRequest struct { func (x *MintScopedStreamRequest) Reset() { *x = MintScopedStreamRequest{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[28] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1678,7 +1794,7 @@ func (x *MintScopedStreamRequest) String() string { func (*MintScopedStreamRequest) ProtoMessage() {} func (x *MintScopedStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[28] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1691,7 +1807,7 @@ func (x *MintScopedStreamRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MintScopedStreamRequest.ProtoReflect.Descriptor instead. func (*MintScopedStreamRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{28} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{29} } func (x *MintScopedStreamRequest) GetMediaFileId() int64 { @@ -1789,7 +1905,7 @@ type MintScopedStreamResponse struct { func (x *MintScopedStreamResponse) Reset() { *x = MintScopedStreamResponse{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[29] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1801,7 +1917,7 @@ func (x *MintScopedStreamResponse) String() string { func (*MintScopedStreamResponse) ProtoMessage() {} func (x *MintScopedStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[29] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1814,7 +1930,7 @@ func (x *MintScopedStreamResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MintScopedStreamResponse.ProtoReflect.Descriptor instead. func (*MintScopedStreamResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{29} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{30} } func (x *MintScopedStreamResponse) GetStreamUrl() string { @@ -1852,7 +1968,7 @@ type CallPluginHTTPRequest struct { func (x *CallPluginHTTPRequest) Reset() { *x = CallPluginHTTPRequest{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[30] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1864,7 +1980,7 @@ func (x *CallPluginHTTPRequest) String() string { func (*CallPluginHTTPRequest) ProtoMessage() {} func (x *CallPluginHTTPRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[30] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1877,7 +1993,7 @@ func (x *CallPluginHTTPRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CallPluginHTTPRequest.ProtoReflect.Descriptor instead. func (*CallPluginHTTPRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{30} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{31} } func (x *CallPluginHTTPRequest) GetInstallationId() int32 { @@ -1933,7 +2049,7 @@ type CallPluginHTTPResponse struct { func (x *CallPluginHTTPResponse) Reset() { *x = CallPluginHTTPResponse{} - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[31] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1945,7 +2061,7 @@ func (x *CallPluginHTTPResponse) String() string { func (*CallPluginHTTPResponse) ProtoMessage() {} func (x *CallPluginHTTPResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[31] + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1958,7 +2074,7 @@ func (x *CallPluginHTTPResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CallPluginHTTPResponse.ProtoReflect.Descriptor instead. func (*CallPluginHTTPResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{31} + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{32} } func (x *CallPluginHTTPResponse) GetStatusCode() int32 { @@ -1982,11 +2098,280 @@ func (x *CallPluginHTTPResponse) GetBody() []byte { return nil } +type ReadInstanceStateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // State key, <= 256 bytes. + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReadInstanceStateRequest) Reset() { + *x = ReadInstanceStateRequest{} + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadInstanceStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadInstanceStateRequest) ProtoMessage() {} + +func (x *ReadInstanceStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadInstanceStateRequest.ProtoReflect.Descriptor instead. +func (*ReadInstanceStateRequest) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{33} +} + +func (x *ReadInstanceStateRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +type ReadInstanceStateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stored value; empty when found is false. + Value []byte `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + // False when no value is stored under key in this instance's scope. + Found bool `protobuf:"varint,2,opt,name=found,proto3" json:"found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReadInstanceStateResponse) Reset() { + *x = ReadInstanceStateResponse{} + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadInstanceStateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadInstanceStateResponse) ProtoMessage() {} + +func (x *ReadInstanceStateResponse) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadInstanceStateResponse.ProtoReflect.Descriptor instead. +func (*ReadInstanceStateResponse) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{34} +} + +func (x *ReadInstanceStateResponse) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +func (x *ReadInstanceStateResponse) GetFound() bool { + if x != nil { + return x.Found + } + return false +} + +type WriteInstanceStateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // State key, <= 256 bytes. + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + // Value to store, <= 256 KiB. + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WriteInstanceStateRequest) Reset() { + *x = WriteInstanceStateRequest{} + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WriteInstanceStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteInstanceStateRequest) ProtoMessage() {} + +func (x *WriteInstanceStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WriteInstanceStateRequest.ProtoReflect.Descriptor instead. +func (*WriteInstanceStateRequest) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{35} +} + +func (x *WriteInstanceStateRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *WriteInstanceStateRequest) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +type WriteInstanceStateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WriteInstanceStateResponse) Reset() { + *x = WriteInstanceStateResponse{} + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WriteInstanceStateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteInstanceStateResponse) ProtoMessage() {} + +func (x *WriteInstanceStateResponse) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WriteInstanceStateResponse.ProtoReflect.Descriptor instead. +func (*WriteInstanceStateResponse) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{36} +} + +type ReportNetworkAccessStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status *NetworkAccessStatus `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportNetworkAccessStatusRequest) Reset() { + *x = ReportNetworkAccessStatusRequest{} + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportNetworkAccessStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportNetworkAccessStatusRequest) ProtoMessage() {} + +func (x *ReportNetworkAccessStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReportNetworkAccessStatusRequest.ProtoReflect.Descriptor instead. +func (*ReportNetworkAccessStatusRequest) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{37} +} + +func (x *ReportNetworkAccessStatusRequest) GetStatus() *NetworkAccessStatus { + if x != nil { + return x.Status + } + return nil +} + +type ReportNetworkAccessStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportNetworkAccessStatusResponse) Reset() { + *x = ReportNetworkAccessStatusResponse{} + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportNetworkAccessStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportNetworkAccessStatusResponse) ProtoMessage() {} + +func (x *ReportNetworkAccessStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_runtime_host_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReportNetworkAccessStatusResponse.ProtoReflect.Descriptor instead. +func (*ReportNetworkAccessStatusResponse) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_runtime_host_proto_rawDescGZIP(), []int{38} +} + var File_silo_plugin_v1_runtime_host_proto protoreflect.FileDescriptor const file_silo_plugin_v1_runtime_host_proto_rawDesc = "" + "\n" + - "!silo/plugin/v1/runtime_host.proto\x12\x0esilo.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1bsilo/plugin/v1/common.proto\"g\n" + + "!silo/plugin/v1/runtime_host.proto\x12\x0esilo.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1bsilo/plugin/v1/common.proto\x1a,silo/plugin/v1/network_access_provider.proto\"g\n" + "\x13PublishEventRequest\x12\x1d\n" + "\n" + "event_name\x18\x01 \x01(\tR\teventName\x121\n" + @@ -2004,11 +2389,20 @@ const file_silo_plugin_v1_runtime_host_proto_rawDesc = "" + "event_name\x18\x02 \x01(\tR\teventName\x121\n" + "\apayload\x18\x03 \x01(\v2\x17.google.protobuf.StructR\apayload\"$\n" + "\"PublishEventToInstallationResponse\"\x14\n" + - "\x12GetHostInfoRequest\"\x9c\x01\n" + + "\x12GetHostInfoRequest\"\xd0\x02\n" + "\x13GetHostInfoResponse\x12&\n" + "\x0fpublic_base_url\x18\x01 \x01(\tR\rpublicBaseUrl\x12*\n" + "\x11internal_base_url\x18\x02 \x01(\tR\x0finternalBaseUrl\x121\n" + - "\x15plugin_proxy_base_url\x18\x03 \x01(\tR\x12pluginProxyBaseUrl\"/\n" + + "\x15plugin_proxy_base_url\x18\x03 \x01(\tR\x12pluginProxyBaseUrl\x12\x1b\n" + + "\thost_role\x18\x04 \x01(\tR\bhostRole\x12\x1b\n" + + "\thost_name\x18\x05 \x01(\tR\bhostName\x12\x17\n" + + "\anode_id\x18\x06 \x01(\x03R\x06nodeId\x12#\n" + + "\ringress_token\x18\a \x01(\tR\fingressToken\x12:\n" + + "\tlisteners\x18\b \x03(\v2\x1c.silo.plugin.v1.HostListenerR\tlisteners\"_\n" + + "\fHostListener\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + + "\aaddress\x18\x02 \x01(\tR\aaddress\x12!\n" + + "\fdefault_port\x18\x03 \x01(\x05R\vdefaultPort\"/\n" + "\x14ListLibrariesRequest\x12\x17\n" + "\auser_id\x18\x01 \x01(\tR\x06userId\"N\n" + "\x15ListLibrariesResponse\x125\n" + @@ -2153,8 +2547,19 @@ const file_silo_plugin_v1_runtime_host_proto_rawDesc = "" + "\x04body\x18\x03 \x01(\fR\x04body\x1a:\n" + "\fHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x012\xe7\n" + - "\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\",\n" + + "\x18ReadInstanceStateRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\"G\n" + + "\x19ReadInstanceStateResponse\x12\x14\n" + + "\x05value\x18\x01 \x01(\fR\x05value\x12\x14\n" + + "\x05found\x18\x02 \x01(\bR\x05found\"C\n" + + "\x19WriteInstanceStateRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\fR\x05value\"\x1c\n" + + "\x1aWriteInstanceStateResponse\"_\n" + + " ReportNetworkAccessStatusRequest\x12;\n" + + "\x06status\x18\x01 \x01(\v2#.silo.plugin.v1.NetworkAccessStatusR\x06status\"#\n" + + "!ReportNetworkAccessStatusResponse2\xc1\r\n" + "\vRuntimeHost\x12Y\n" + "\fPublishEvent\x12#.silo.plugin.v1.PublishEventRequest\x1a$.silo.plugin.v1.PublishEventResponse\x12_\n" + "\x0ePublishEventTo\x12%.silo.plugin.v1.PublishEventToRequest\x1a&.silo.plugin.v1.PublishEventToResponse\x12\x83\x01\n" + @@ -2168,7 +2573,10 @@ const file_silo_plugin_v1_runtime_host_proto_rawDesc = "" + "\x0fGetCatalogStats\x12&.silo.plugin.v1.GetCatalogStatsRequest\x1a'.silo.plugin.v1.GetCatalogStatsResponse\x12z\n" + "\x17ResolveCatalogImageURLs\x12..silo.plugin.v1.ResolveCatalogImageURLsRequest\x1a/.silo.plugin.v1.ResolveCatalogImageURLsResponse\x12e\n" + "\x10MintScopedStream\x12'.silo.plugin.v1.MintScopedStreamRequest\x1a(.silo.plugin.v1.MintScopedStreamResponse\x12_\n" + - "\x0eCallPluginHTTP\x12%.silo.plugin.v1.CallPluginHTTPRequest\x1a&.silo.plugin.v1.CallPluginHTTPResponseBPZNgithub.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1;pluginv1b\x06proto3" + "\x0eCallPluginHTTP\x12%.silo.plugin.v1.CallPluginHTTPRequest\x1a&.silo.plugin.v1.CallPluginHTTPResponse\x12h\n" + + "\x11ReadInstanceState\x12(.silo.plugin.v1.ReadInstanceStateRequest\x1a).silo.plugin.v1.ReadInstanceStateResponse\x12k\n" + + "\x12WriteInstanceState\x12).silo.plugin.v1.WriteInstanceStateRequest\x1a*.silo.plugin.v1.WriteInstanceStateResponse\x12\x80\x01\n" + + "\x19ReportNetworkAccessStatus\x120.silo.plugin.v1.ReportNetworkAccessStatusRequest\x1a1.silo.plugin.v1.ReportNetworkAccessStatusResponseBPZNgithub.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1;pluginv1b\x06proto3" var ( file_silo_plugin_v1_runtime_host_proto_rawDescOnce sync.Once @@ -2182,7 +2590,7 @@ func file_silo_plugin_v1_runtime_host_proto_rawDescGZIP() []byte { return file_silo_plugin_v1_runtime_host_proto_rawDescData } -var file_silo_plugin_v1_runtime_host_proto_msgTypes = make([]protoimpl.MessageInfo, 35) +var file_silo_plugin_v1_runtime_host_proto_msgTypes = make([]protoimpl.MessageInfo, 42) var file_silo_plugin_v1_runtime_host_proto_goTypes = []any{ (*PublishEventRequest)(nil), // 0: silo.plugin.v1.PublishEventRequest (*PublishEventResponse)(nil), // 1: silo.plugin.v1.PublishEventResponse @@ -2192,83 +2600,99 @@ var file_silo_plugin_v1_runtime_host_proto_goTypes = []any{ (*PublishEventToInstallationResponse)(nil), // 5: silo.plugin.v1.PublishEventToInstallationResponse (*GetHostInfoRequest)(nil), // 6: silo.plugin.v1.GetHostInfoRequest (*GetHostInfoResponse)(nil), // 7: silo.plugin.v1.GetHostInfoResponse - (*ListLibrariesRequest)(nil), // 8: silo.plugin.v1.ListLibrariesRequest - (*ListLibrariesResponse)(nil), // 9: silo.plugin.v1.ListLibrariesResponse - (*Library)(nil), // 10: silo.plugin.v1.Library - (*CheckMediaPresenceRequest)(nil), // 11: silo.plugin.v1.CheckMediaPresenceRequest - (*CheckMediaPresenceResponse)(nil), // 12: silo.plugin.v1.CheckMediaPresenceResponse - (*MediaPresence)(nil), // 13: silo.plugin.v1.MediaPresence - (*ListInstalledPluginsRequest)(nil), // 14: silo.plugin.v1.ListInstalledPluginsRequest - (*ListInstalledPluginsResponse)(nil), // 15: silo.plugin.v1.ListInstalledPluginsResponse - (*InstalledPlugin)(nil), // 16: silo.plugin.v1.InstalledPlugin - (*SetGlobalConfigEntryRequest)(nil), // 17: silo.plugin.v1.SetGlobalConfigEntryRequest - (*SetGlobalConfigEntryResponse)(nil), // 18: silo.plugin.v1.SetGlobalConfigEntryResponse - (*ListLibraryMediaRequest)(nil), // 19: silo.plugin.v1.ListLibraryMediaRequest - (*ListLibraryMediaResponse)(nil), // 20: silo.plugin.v1.ListLibraryMediaResponse - (*CatalogMediaItem)(nil), // 21: silo.plugin.v1.CatalogMediaItem - (*GetCatalogStatsRequest)(nil), // 22: silo.plugin.v1.GetCatalogStatsRequest - (*GetCatalogStatsResponse)(nil), // 23: silo.plugin.v1.GetCatalogStatsResponse - (*CatalogTypeCount)(nil), // 24: silo.plugin.v1.CatalogTypeCount - (*CatalogLibraryCount)(nil), // 25: silo.plugin.v1.CatalogLibraryCount - (*ResolveCatalogImageURLsRequest)(nil), // 26: silo.plugin.v1.ResolveCatalogImageURLsRequest - (*ResolveCatalogImageURLsResponse)(nil), // 27: silo.plugin.v1.ResolveCatalogImageURLsResponse - (*MintScopedStreamRequest)(nil), // 28: silo.plugin.v1.MintScopedStreamRequest - (*MintScopedStreamResponse)(nil), // 29: silo.plugin.v1.MintScopedStreamResponse - (*CallPluginHTTPRequest)(nil), // 30: silo.plugin.v1.CallPluginHTTPRequest - (*CallPluginHTTPResponse)(nil), // 31: silo.plugin.v1.CallPluginHTTPResponse - nil, // 32: silo.plugin.v1.ResolveCatalogImageURLsResponse.UrlsEntry - nil, // 33: silo.plugin.v1.CallPluginHTTPRequest.HeadersEntry - nil, // 34: silo.plugin.v1.CallPluginHTTPResponse.HeadersEntry - (*structpb.Struct)(nil), // 35: google.protobuf.Struct - (*CapabilityDescriptor)(nil), // 36: silo.plugin.v1.CapabilityDescriptor + (*HostListener)(nil), // 8: silo.plugin.v1.HostListener + (*ListLibrariesRequest)(nil), // 9: silo.plugin.v1.ListLibrariesRequest + (*ListLibrariesResponse)(nil), // 10: silo.plugin.v1.ListLibrariesResponse + (*Library)(nil), // 11: silo.plugin.v1.Library + (*CheckMediaPresenceRequest)(nil), // 12: silo.plugin.v1.CheckMediaPresenceRequest + (*CheckMediaPresenceResponse)(nil), // 13: silo.plugin.v1.CheckMediaPresenceResponse + (*MediaPresence)(nil), // 14: silo.plugin.v1.MediaPresence + (*ListInstalledPluginsRequest)(nil), // 15: silo.plugin.v1.ListInstalledPluginsRequest + (*ListInstalledPluginsResponse)(nil), // 16: silo.plugin.v1.ListInstalledPluginsResponse + (*InstalledPlugin)(nil), // 17: silo.plugin.v1.InstalledPlugin + (*SetGlobalConfigEntryRequest)(nil), // 18: silo.plugin.v1.SetGlobalConfigEntryRequest + (*SetGlobalConfigEntryResponse)(nil), // 19: silo.plugin.v1.SetGlobalConfigEntryResponse + (*ListLibraryMediaRequest)(nil), // 20: silo.plugin.v1.ListLibraryMediaRequest + (*ListLibraryMediaResponse)(nil), // 21: silo.plugin.v1.ListLibraryMediaResponse + (*CatalogMediaItem)(nil), // 22: silo.plugin.v1.CatalogMediaItem + (*GetCatalogStatsRequest)(nil), // 23: silo.plugin.v1.GetCatalogStatsRequest + (*GetCatalogStatsResponse)(nil), // 24: silo.plugin.v1.GetCatalogStatsResponse + (*CatalogTypeCount)(nil), // 25: silo.plugin.v1.CatalogTypeCount + (*CatalogLibraryCount)(nil), // 26: silo.plugin.v1.CatalogLibraryCount + (*ResolveCatalogImageURLsRequest)(nil), // 27: silo.plugin.v1.ResolveCatalogImageURLsRequest + (*ResolveCatalogImageURLsResponse)(nil), // 28: silo.plugin.v1.ResolveCatalogImageURLsResponse + (*MintScopedStreamRequest)(nil), // 29: silo.plugin.v1.MintScopedStreamRequest + (*MintScopedStreamResponse)(nil), // 30: silo.plugin.v1.MintScopedStreamResponse + (*CallPluginHTTPRequest)(nil), // 31: silo.plugin.v1.CallPluginHTTPRequest + (*CallPluginHTTPResponse)(nil), // 32: silo.plugin.v1.CallPluginHTTPResponse + (*ReadInstanceStateRequest)(nil), // 33: silo.plugin.v1.ReadInstanceStateRequest + (*ReadInstanceStateResponse)(nil), // 34: silo.plugin.v1.ReadInstanceStateResponse + (*WriteInstanceStateRequest)(nil), // 35: silo.plugin.v1.WriteInstanceStateRequest + (*WriteInstanceStateResponse)(nil), // 36: silo.plugin.v1.WriteInstanceStateResponse + (*ReportNetworkAccessStatusRequest)(nil), // 37: silo.plugin.v1.ReportNetworkAccessStatusRequest + (*ReportNetworkAccessStatusResponse)(nil), // 38: silo.plugin.v1.ReportNetworkAccessStatusResponse + nil, // 39: silo.plugin.v1.ResolveCatalogImageURLsResponse.UrlsEntry + nil, // 40: silo.plugin.v1.CallPluginHTTPRequest.HeadersEntry + nil, // 41: silo.plugin.v1.CallPluginHTTPResponse.HeadersEntry + (*structpb.Struct)(nil), // 42: google.protobuf.Struct + (*CapabilityDescriptor)(nil), // 43: silo.plugin.v1.CapabilityDescriptor + (*NetworkAccessStatus)(nil), // 44: silo.plugin.v1.NetworkAccessStatus } var file_silo_plugin_v1_runtime_host_proto_depIdxs = []int32{ - 35, // 0: silo.plugin.v1.PublishEventRequest.payload:type_name -> google.protobuf.Struct - 35, // 1: silo.plugin.v1.PublishEventToRequest.payload:type_name -> google.protobuf.Struct - 35, // 2: silo.plugin.v1.PublishEventToInstallationRequest.payload:type_name -> google.protobuf.Struct - 10, // 3: silo.plugin.v1.ListLibrariesResponse.libraries:type_name -> silo.plugin.v1.Library - 13, // 4: silo.plugin.v1.CheckMediaPresenceResponse.present:type_name -> silo.plugin.v1.MediaPresence - 16, // 5: silo.plugin.v1.ListInstalledPluginsResponse.plugins:type_name -> silo.plugin.v1.InstalledPlugin - 36, // 6: silo.plugin.v1.InstalledPlugin.capabilities:type_name -> silo.plugin.v1.CapabilityDescriptor - 35, // 7: silo.plugin.v1.SetGlobalConfigEntryRequest.value:type_name -> google.protobuf.Struct - 21, // 8: silo.plugin.v1.ListLibraryMediaResponse.items:type_name -> silo.plugin.v1.CatalogMediaItem - 24, // 9: silo.plugin.v1.GetCatalogStatsResponse.media_type_counts:type_name -> silo.plugin.v1.CatalogTypeCount - 25, // 10: silo.plugin.v1.GetCatalogStatsResponse.library_counts:type_name -> silo.plugin.v1.CatalogLibraryCount - 32, // 11: silo.plugin.v1.ResolveCatalogImageURLsResponse.urls:type_name -> silo.plugin.v1.ResolveCatalogImageURLsResponse.UrlsEntry - 33, // 12: silo.plugin.v1.CallPluginHTTPRequest.headers:type_name -> silo.plugin.v1.CallPluginHTTPRequest.HeadersEntry - 35, // 13: silo.plugin.v1.CallPluginHTTPRequest.query:type_name -> google.protobuf.Struct - 34, // 14: silo.plugin.v1.CallPluginHTTPResponse.headers:type_name -> silo.plugin.v1.CallPluginHTTPResponse.HeadersEntry - 0, // 15: silo.plugin.v1.RuntimeHost.PublishEvent:input_type -> silo.plugin.v1.PublishEventRequest - 2, // 16: silo.plugin.v1.RuntimeHost.PublishEventTo:input_type -> silo.plugin.v1.PublishEventToRequest - 4, // 17: silo.plugin.v1.RuntimeHost.PublishEventToInstallation:input_type -> silo.plugin.v1.PublishEventToInstallationRequest - 6, // 18: silo.plugin.v1.RuntimeHost.GetHostInfo:input_type -> silo.plugin.v1.GetHostInfoRequest - 8, // 19: silo.plugin.v1.RuntimeHost.ListLibraries:input_type -> silo.plugin.v1.ListLibrariesRequest - 11, // 20: silo.plugin.v1.RuntimeHost.CheckMediaPresence:input_type -> silo.plugin.v1.CheckMediaPresenceRequest - 14, // 21: silo.plugin.v1.RuntimeHost.ListInstalledPlugins:input_type -> silo.plugin.v1.ListInstalledPluginsRequest - 17, // 22: silo.plugin.v1.RuntimeHost.SetGlobalConfigEntry:input_type -> silo.plugin.v1.SetGlobalConfigEntryRequest - 19, // 23: silo.plugin.v1.RuntimeHost.ListLibraryMedia:input_type -> silo.plugin.v1.ListLibraryMediaRequest - 22, // 24: silo.plugin.v1.RuntimeHost.GetCatalogStats:input_type -> silo.plugin.v1.GetCatalogStatsRequest - 26, // 25: silo.plugin.v1.RuntimeHost.ResolveCatalogImageURLs:input_type -> silo.plugin.v1.ResolveCatalogImageURLsRequest - 28, // 26: silo.plugin.v1.RuntimeHost.MintScopedStream:input_type -> silo.plugin.v1.MintScopedStreamRequest - 30, // 27: silo.plugin.v1.RuntimeHost.CallPluginHTTP:input_type -> silo.plugin.v1.CallPluginHTTPRequest - 1, // 28: silo.plugin.v1.RuntimeHost.PublishEvent:output_type -> silo.plugin.v1.PublishEventResponse - 3, // 29: silo.plugin.v1.RuntimeHost.PublishEventTo:output_type -> silo.plugin.v1.PublishEventToResponse - 5, // 30: silo.plugin.v1.RuntimeHost.PublishEventToInstallation:output_type -> silo.plugin.v1.PublishEventToInstallationResponse - 7, // 31: silo.plugin.v1.RuntimeHost.GetHostInfo:output_type -> silo.plugin.v1.GetHostInfoResponse - 9, // 32: silo.plugin.v1.RuntimeHost.ListLibraries:output_type -> silo.plugin.v1.ListLibrariesResponse - 12, // 33: silo.plugin.v1.RuntimeHost.CheckMediaPresence:output_type -> silo.plugin.v1.CheckMediaPresenceResponse - 15, // 34: silo.plugin.v1.RuntimeHost.ListInstalledPlugins:output_type -> silo.plugin.v1.ListInstalledPluginsResponse - 18, // 35: silo.plugin.v1.RuntimeHost.SetGlobalConfigEntry:output_type -> silo.plugin.v1.SetGlobalConfigEntryResponse - 20, // 36: silo.plugin.v1.RuntimeHost.ListLibraryMedia:output_type -> silo.plugin.v1.ListLibraryMediaResponse - 23, // 37: silo.plugin.v1.RuntimeHost.GetCatalogStats:output_type -> silo.plugin.v1.GetCatalogStatsResponse - 27, // 38: silo.plugin.v1.RuntimeHost.ResolveCatalogImageURLs:output_type -> silo.plugin.v1.ResolveCatalogImageURLsResponse - 29, // 39: silo.plugin.v1.RuntimeHost.MintScopedStream:output_type -> silo.plugin.v1.MintScopedStreamResponse - 31, // 40: silo.plugin.v1.RuntimeHost.CallPluginHTTP:output_type -> silo.plugin.v1.CallPluginHTTPResponse - 28, // [28:41] is the sub-list for method output_type - 15, // [15:28] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 42, // 0: silo.plugin.v1.PublishEventRequest.payload:type_name -> google.protobuf.Struct + 42, // 1: silo.plugin.v1.PublishEventToRequest.payload:type_name -> google.protobuf.Struct + 42, // 2: silo.plugin.v1.PublishEventToInstallationRequest.payload:type_name -> google.protobuf.Struct + 8, // 3: silo.plugin.v1.GetHostInfoResponse.listeners:type_name -> silo.plugin.v1.HostListener + 11, // 4: silo.plugin.v1.ListLibrariesResponse.libraries:type_name -> silo.plugin.v1.Library + 14, // 5: silo.plugin.v1.CheckMediaPresenceResponse.present:type_name -> silo.plugin.v1.MediaPresence + 17, // 6: silo.plugin.v1.ListInstalledPluginsResponse.plugins:type_name -> silo.plugin.v1.InstalledPlugin + 43, // 7: silo.plugin.v1.InstalledPlugin.capabilities:type_name -> silo.plugin.v1.CapabilityDescriptor + 42, // 8: silo.plugin.v1.SetGlobalConfigEntryRequest.value:type_name -> google.protobuf.Struct + 22, // 9: silo.plugin.v1.ListLibraryMediaResponse.items:type_name -> silo.plugin.v1.CatalogMediaItem + 25, // 10: silo.plugin.v1.GetCatalogStatsResponse.media_type_counts:type_name -> silo.plugin.v1.CatalogTypeCount + 26, // 11: silo.plugin.v1.GetCatalogStatsResponse.library_counts:type_name -> silo.plugin.v1.CatalogLibraryCount + 39, // 12: silo.plugin.v1.ResolveCatalogImageURLsResponse.urls:type_name -> silo.plugin.v1.ResolveCatalogImageURLsResponse.UrlsEntry + 40, // 13: silo.plugin.v1.CallPluginHTTPRequest.headers:type_name -> silo.plugin.v1.CallPluginHTTPRequest.HeadersEntry + 42, // 14: silo.plugin.v1.CallPluginHTTPRequest.query:type_name -> google.protobuf.Struct + 41, // 15: silo.plugin.v1.CallPluginHTTPResponse.headers:type_name -> silo.plugin.v1.CallPluginHTTPResponse.HeadersEntry + 44, // 16: silo.plugin.v1.ReportNetworkAccessStatusRequest.status:type_name -> silo.plugin.v1.NetworkAccessStatus + 0, // 17: silo.plugin.v1.RuntimeHost.PublishEvent:input_type -> silo.plugin.v1.PublishEventRequest + 2, // 18: silo.plugin.v1.RuntimeHost.PublishEventTo:input_type -> silo.plugin.v1.PublishEventToRequest + 4, // 19: silo.plugin.v1.RuntimeHost.PublishEventToInstallation:input_type -> silo.plugin.v1.PublishEventToInstallationRequest + 6, // 20: silo.plugin.v1.RuntimeHost.GetHostInfo:input_type -> silo.plugin.v1.GetHostInfoRequest + 9, // 21: silo.plugin.v1.RuntimeHost.ListLibraries:input_type -> silo.plugin.v1.ListLibrariesRequest + 12, // 22: silo.plugin.v1.RuntimeHost.CheckMediaPresence:input_type -> silo.plugin.v1.CheckMediaPresenceRequest + 15, // 23: silo.plugin.v1.RuntimeHost.ListInstalledPlugins:input_type -> silo.plugin.v1.ListInstalledPluginsRequest + 18, // 24: silo.plugin.v1.RuntimeHost.SetGlobalConfigEntry:input_type -> silo.plugin.v1.SetGlobalConfigEntryRequest + 20, // 25: silo.plugin.v1.RuntimeHost.ListLibraryMedia:input_type -> silo.plugin.v1.ListLibraryMediaRequest + 23, // 26: silo.plugin.v1.RuntimeHost.GetCatalogStats:input_type -> silo.plugin.v1.GetCatalogStatsRequest + 27, // 27: silo.plugin.v1.RuntimeHost.ResolveCatalogImageURLs:input_type -> silo.plugin.v1.ResolveCatalogImageURLsRequest + 29, // 28: silo.plugin.v1.RuntimeHost.MintScopedStream:input_type -> silo.plugin.v1.MintScopedStreamRequest + 31, // 29: silo.plugin.v1.RuntimeHost.CallPluginHTTP:input_type -> silo.plugin.v1.CallPluginHTTPRequest + 33, // 30: silo.plugin.v1.RuntimeHost.ReadInstanceState:input_type -> silo.plugin.v1.ReadInstanceStateRequest + 35, // 31: silo.plugin.v1.RuntimeHost.WriteInstanceState:input_type -> silo.plugin.v1.WriteInstanceStateRequest + 37, // 32: silo.plugin.v1.RuntimeHost.ReportNetworkAccessStatus:input_type -> silo.plugin.v1.ReportNetworkAccessStatusRequest + 1, // 33: silo.plugin.v1.RuntimeHost.PublishEvent:output_type -> silo.plugin.v1.PublishEventResponse + 3, // 34: silo.plugin.v1.RuntimeHost.PublishEventTo:output_type -> silo.plugin.v1.PublishEventToResponse + 5, // 35: silo.plugin.v1.RuntimeHost.PublishEventToInstallation:output_type -> silo.plugin.v1.PublishEventToInstallationResponse + 7, // 36: silo.plugin.v1.RuntimeHost.GetHostInfo:output_type -> silo.plugin.v1.GetHostInfoResponse + 10, // 37: silo.plugin.v1.RuntimeHost.ListLibraries:output_type -> silo.plugin.v1.ListLibrariesResponse + 13, // 38: silo.plugin.v1.RuntimeHost.CheckMediaPresence:output_type -> silo.plugin.v1.CheckMediaPresenceResponse + 16, // 39: silo.plugin.v1.RuntimeHost.ListInstalledPlugins:output_type -> silo.plugin.v1.ListInstalledPluginsResponse + 19, // 40: silo.plugin.v1.RuntimeHost.SetGlobalConfigEntry:output_type -> silo.plugin.v1.SetGlobalConfigEntryResponse + 21, // 41: silo.plugin.v1.RuntimeHost.ListLibraryMedia:output_type -> silo.plugin.v1.ListLibraryMediaResponse + 24, // 42: silo.plugin.v1.RuntimeHost.GetCatalogStats:output_type -> silo.plugin.v1.GetCatalogStatsResponse + 28, // 43: silo.plugin.v1.RuntimeHost.ResolveCatalogImageURLs:output_type -> silo.plugin.v1.ResolveCatalogImageURLsResponse + 30, // 44: silo.plugin.v1.RuntimeHost.MintScopedStream:output_type -> silo.plugin.v1.MintScopedStreamResponse + 32, // 45: silo.plugin.v1.RuntimeHost.CallPluginHTTP:output_type -> silo.plugin.v1.CallPluginHTTPResponse + 34, // 46: silo.plugin.v1.RuntimeHost.ReadInstanceState:output_type -> silo.plugin.v1.ReadInstanceStateResponse + 36, // 47: silo.plugin.v1.RuntimeHost.WriteInstanceState:output_type -> silo.plugin.v1.WriteInstanceStateResponse + 38, // 48: silo.plugin.v1.RuntimeHost.ReportNetworkAccessStatus:output_type -> silo.plugin.v1.ReportNetworkAccessStatusResponse + 33, // [33:49] is the sub-list for method output_type + 17, // [17:33] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_silo_plugin_v1_runtime_host_proto_init() } @@ -2277,13 +2701,14 @@ func file_silo_plugin_v1_runtime_host_proto_init() { return } file_silo_plugin_v1_common_proto_init() + file_silo_plugin_v1_network_access_provider_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_silo_plugin_v1_runtime_host_proto_rawDesc), len(file_silo_plugin_v1_runtime_host_proto_rawDesc)), NumEnums: 0, - NumMessages: 35, + NumMessages: 42, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/pluginproto/silo/plugin/v1/runtime_host_grpc.pb.go b/pkg/pluginproto/silo/plugin/v1/runtime_host_grpc.pb.go index 77b0fa6..ee216c3 100644 --- a/pkg/pluginproto/silo/plugin/v1/runtime_host_grpc.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/runtime_host_grpc.pb.go @@ -32,6 +32,9 @@ const ( RuntimeHost_ResolveCatalogImageURLs_FullMethodName = "/silo.plugin.v1.RuntimeHost/ResolveCatalogImageURLs" RuntimeHost_MintScopedStream_FullMethodName = "/silo.plugin.v1.RuntimeHost/MintScopedStream" RuntimeHost_CallPluginHTTP_FullMethodName = "/silo.plugin.v1.RuntimeHost/CallPluginHTTP" + RuntimeHost_ReadInstanceState_FullMethodName = "/silo.plugin.v1.RuntimeHost/ReadInstanceState" + RuntimeHost_WriteInstanceState_FullMethodName = "/silo.plugin.v1.RuntimeHost/WriteInstanceState" + RuntimeHost_ReportNetworkAccessStatus_FullMethodName = "/silo.plugin.v1.RuntimeHost/ReportNetworkAccessStatus" ) // RuntimeHostClient is the client API for RuntimeHost service. @@ -83,6 +86,18 @@ type RuntimeHostClient interface { // through the host control plane. Calls are treated as authenticated // service-to-service traffic, but never as admin traffic. CallPluginHTTP(ctx context.Context, in *CallPluginHTTPRequest, opts ...grpc.CallOption) (*CallPluginHTTPResponse, error) + // ReadInstanceState reads one key of the calling plugin instance's private + // state. The scope (installation plus host) is derived by the host and never + // supplied by the plugin. See docs/network-access-provider.md. + ReadInstanceState(ctx context.Context, in *ReadInstanceStateRequest, opts ...grpc.CallOption) (*ReadInstanceStateResponse, error) + // WriteInstanceState writes one key of the calling plugin instance's private + // state. The host stores values encrypted and never returns them through + // any API. Limits: key <= 256 bytes, value <= 256 KiB, <= 256 keys per scope. + WriteInstanceState(ctx context.Context, in *WriteInstanceStateRequest, opts ...grpc.CallOption) (*WriteInstanceStateResponse, error) + // ReportNetworkAccessStatus pushes a network_access_provider.v1 status + // change to the host so it does not have to poll. Plugins call it on every + // state transition; the host may still call GetStatus on demand. + ReportNetworkAccessStatus(ctx context.Context, in *ReportNetworkAccessStatusRequest, opts ...grpc.CallOption) (*ReportNetworkAccessStatusResponse, error) } type runtimeHostClient struct { @@ -223,6 +238,36 @@ func (c *runtimeHostClient) CallPluginHTTP(ctx context.Context, in *CallPluginHT return out, nil } +func (c *runtimeHostClient) ReadInstanceState(ctx context.Context, in *ReadInstanceStateRequest, opts ...grpc.CallOption) (*ReadInstanceStateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReadInstanceStateResponse) + err := c.cc.Invoke(ctx, RuntimeHost_ReadInstanceState_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *runtimeHostClient) WriteInstanceState(ctx context.Context, in *WriteInstanceStateRequest, opts ...grpc.CallOption) (*WriteInstanceStateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(WriteInstanceStateResponse) + err := c.cc.Invoke(ctx, RuntimeHost_WriteInstanceState_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *runtimeHostClient) ReportNetworkAccessStatus(ctx context.Context, in *ReportNetworkAccessStatusRequest, opts ...grpc.CallOption) (*ReportNetworkAccessStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReportNetworkAccessStatusResponse) + err := c.cc.Invoke(ctx, RuntimeHost_ReportNetworkAccessStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // RuntimeHostServer is the server API for RuntimeHost service. // All implementations should embed UnimplementedRuntimeHostServer // for forward compatibility. @@ -272,6 +317,18 @@ type RuntimeHostServer interface { // through the host control plane. Calls are treated as authenticated // service-to-service traffic, but never as admin traffic. CallPluginHTTP(context.Context, *CallPluginHTTPRequest) (*CallPluginHTTPResponse, error) + // ReadInstanceState reads one key of the calling plugin instance's private + // state. The scope (installation plus host) is derived by the host and never + // supplied by the plugin. See docs/network-access-provider.md. + ReadInstanceState(context.Context, *ReadInstanceStateRequest) (*ReadInstanceStateResponse, error) + // WriteInstanceState writes one key of the calling plugin instance's private + // state. The host stores values encrypted and never returns them through + // any API. Limits: key <= 256 bytes, value <= 256 KiB, <= 256 keys per scope. + WriteInstanceState(context.Context, *WriteInstanceStateRequest) (*WriteInstanceStateResponse, error) + // ReportNetworkAccessStatus pushes a network_access_provider.v1 status + // change to the host so it does not have to poll. Plugins call it on every + // state transition; the host may still call GetStatus on demand. + ReportNetworkAccessStatus(context.Context, *ReportNetworkAccessStatusRequest) (*ReportNetworkAccessStatusResponse, error) } // UnimplementedRuntimeHostServer should be embedded to have @@ -320,6 +377,15 @@ func (UnimplementedRuntimeHostServer) MintScopedStream(context.Context, *MintSco func (UnimplementedRuntimeHostServer) CallPluginHTTP(context.Context, *CallPluginHTTPRequest) (*CallPluginHTTPResponse, error) { return nil, status.Error(codes.Unimplemented, "method CallPluginHTTP not implemented") } +func (UnimplementedRuntimeHostServer) ReadInstanceState(context.Context, *ReadInstanceStateRequest) (*ReadInstanceStateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReadInstanceState not implemented") +} +func (UnimplementedRuntimeHostServer) WriteInstanceState(context.Context, *WriteInstanceStateRequest) (*WriteInstanceStateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method WriteInstanceState not implemented") +} +func (UnimplementedRuntimeHostServer) ReportNetworkAccessStatus(context.Context, *ReportNetworkAccessStatusRequest) (*ReportNetworkAccessStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReportNetworkAccessStatus not implemented") +} func (UnimplementedRuntimeHostServer) testEmbeddedByValue() {} // UnsafeRuntimeHostServer may be embedded to opt out of forward compatibility for this service. @@ -574,6 +640,60 @@ func _RuntimeHost_CallPluginHTTP_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _RuntimeHost_ReadInstanceState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReadInstanceStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RuntimeHostServer).ReadInstanceState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RuntimeHost_ReadInstanceState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RuntimeHostServer).ReadInstanceState(ctx, req.(*ReadInstanceStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RuntimeHost_WriteInstanceState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WriteInstanceStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RuntimeHostServer).WriteInstanceState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RuntimeHost_WriteInstanceState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RuntimeHostServer).WriteInstanceState(ctx, req.(*WriteInstanceStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RuntimeHost_ReportNetworkAccessStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportNetworkAccessStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RuntimeHostServer).ReportNetworkAccessStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RuntimeHost_ReportNetworkAccessStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RuntimeHostServer).ReportNetworkAccessStatus(ctx, req.(*ReportNetworkAccessStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + // RuntimeHost_ServiceDesc is the grpc.ServiceDesc for RuntimeHost service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -633,6 +753,18 @@ var RuntimeHost_ServiceDesc = grpc.ServiceDesc{ MethodName: "CallPluginHTTP", Handler: _RuntimeHost_CallPluginHTTP_Handler, }, + { + MethodName: "ReadInstanceState", + Handler: _RuntimeHost_ReadInstanceState_Handler, + }, + { + MethodName: "WriteInstanceState", + Handler: _RuntimeHost_WriteInstanceState_Handler, + }, + { + MethodName: "ReportNetworkAccessStatus", + Handler: _RuntimeHost_ReportNetworkAccessStatus_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "silo/plugin/v1/runtime_host.proto", diff --git a/pkg/pluginsdk/capability/capability.go b/pkg/pluginsdk/capability/capability.go index db5e1af..8eb4698 100644 --- a/pkg/pluginsdk/capability/capability.go +++ b/pkg/pluginsdk/capability/capability.go @@ -15,6 +15,9 @@ const ( EbookBackend = "ebook_backend.v1" ScanSource = "scan_source.v1" WatchSyncProvider = "watch_sync_provider.v1" + // NetworkAccessProvider marks a resident plugin that gives the deployment + // an overlay-network identity (Tailscale, NetBird, ...). Added in v0.16.0. + NetworkAccessProvider = "network_access_provider.v1" ) // KnownTypes lists every capability type recognized by this SDK version. @@ -32,4 +35,5 @@ var KnownTypes = []string{ EbookBackend, ScanSource, WatchSyncProvider, + NetworkAccessProvider, } diff --git a/pkg/pluginsdk/capability/capability_test.go b/pkg/pluginsdk/capability/capability_test.go index bd13191..1bd6a64 100644 --- a/pkg/pluginsdk/capability/capability_test.go +++ b/pkg/pluginsdk/capability/capability_test.go @@ -49,3 +49,19 @@ func TestImageResolverIsKnownType(t *testing.T) { t.Fatalf("ImageResolver (%q) missing from KnownTypes %v", ImageResolver, KnownTypes) } } + +func TestNetworkAccessProviderIsKnownType(t *testing.T) { + if NetworkAccessProvider != "network_access_provider.v1" { + t.Fatalf("NetworkAccessProvider const = %q, want %q", NetworkAccessProvider, "network_access_provider.v1") + } + found := false + for _, k := range KnownTypes { + if k == NetworkAccessProvider { + found = true + break + } + } + if !found { + t.Fatalf("NetworkAccessProvider (%q) missing from KnownTypes %v", NetworkAccessProvider, KnownTypes) + } +} diff --git a/pkg/pluginsdk/manifest/manifest.go b/pkg/pluginsdk/manifest/manifest.go index f6de2c0..b826c83 100644 --- a/pkg/pluginsdk/manifest/manifest.go +++ b/pkg/pluginsdk/manifest/manifest.go @@ -113,6 +113,9 @@ func Validate(manifest *pluginv1.PluginManifest) error { if err := validateWatchSyncCapability(capability); err != nil { return err } + if err := validateNetworkAccessCapability(capability); err != nil { + return err + } } for _, schema := range manifest.GlobalConfigSchema { if err := validateConfigSchema(schema); err != nil { @@ -186,6 +189,23 @@ func validateWatchSyncCapability(descriptor *pluginv1.CapabilityDescriptor) erro return nil } +func validateNetworkAccessCapability(descriptor *pluginv1.CapabilityDescriptor) error { + networkAccess := descriptor.GetNetworkAccessProvider() + if descriptor.GetType() != capability.NetworkAccessProvider { + if networkAccess != nil { + return fmt.Errorf("plugin capability %q: network_access_provider descriptor requires type %q", descriptor.GetId(), capability.NetworkAccessProvider) + } + return nil + } + if networkAccess == nil { + return fmt.Errorf("plugin capability %q: network_access_provider descriptor is required", descriptor.GetId()) + } + if !watchSyncSlugPattern.MatchString(networkAccess.GetProvider()) { + return fmt.Errorf("plugin capability %q: network access provider must be a path-safe lowercase slug", descriptor.GetId()) + } + return nil +} + // ValidateCatalogPresentation applies the stricter presentation contract used // by curated catalogs. The general manifest validator keeps presentation // optional so older and directly uploaded plugins remain compatible. diff --git a/pkg/pluginsdk/manifest/network_access_provider_test.go b/pkg/pluginsdk/manifest/network_access_provider_test.go new file mode 100644 index 0000000..b3f2594 --- /dev/null +++ b/pkg/pluginsdk/manifest/network_access_provider_test.go @@ -0,0 +1,62 @@ +package manifest_test + +import ( + "testing" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" + publicmanifest "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/manifest" +) + +func TestLoadNetworkAccessProvider(t *testing.T) { + raw := []byte(`{ + "plugin_id":"silo.tailscale", "version":"1.0.0", "silo_api_version":"v1", + "capabilities":[{ + "type":"network_access_provider.v1", "id":"tailscale", "display_name":"Tailscale", + "network_access_provider":{"provider":"tailscale","display_name":"Tailscale"} + }] + }`) + manifest, err := publicmanifest.Load(raw) + if err != nil { + t.Fatal(err) + } + descriptor := manifest.GetCapabilities()[0].GetNetworkAccessProvider() + if descriptor.GetProvider() != "tailscale" || descriptor.GetDisplayName() != "Tailscale" { + t.Fatalf("network access descriptor = %#v", descriptor) + } +} + +func TestValidateNetworkAccessProviderRejectsMissingDescriptor(t *testing.T) { + manifest := &pluginv1.PluginManifest{ + PluginId: "silo.invalid", Version: "1.0.0", + Capabilities: []*pluginv1.CapabilityDescriptor{{Type: "network_access_provider.v1", Id: "overlay"}}, + } + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected missing network access descriptor to fail") + } +} + +func TestValidateNetworkAccessProviderRejectsBadSlug(t *testing.T) { + manifest := &pluginv1.PluginManifest{ + PluginId: "silo.invalid", Version: "1.0.0", + Capabilities: []*pluginv1.CapabilityDescriptor{{ + Type: "network_access_provider.v1", Id: "overlay", + NetworkAccessProvider: &pluginv1.NetworkAccessProviderDescriptor{Provider: "Tail Scale"}, + }}, + } + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected provider slug with spaces and capitals to fail") + } +} + +func TestValidateNetworkAccessProviderRejectsDescriptorOnOtherType(t *testing.T) { + manifest := &pluginv1.PluginManifest{ + PluginId: "silo.invalid", Version: "1.0.0", + Capabilities: []*pluginv1.CapabilityDescriptor{{ + Type: "scheduled_task.v1", Id: "task", + NetworkAccessProvider: &pluginv1.NetworkAccessProviderDescriptor{Provider: "tailscale"}, + }}, + } + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected network access descriptor on a scheduled task to fail") + } +} diff --git a/pkg/pluginsdk/runtime/capability_servers_compat_test.go b/pkg/pluginsdk/runtime/capability_servers_compat_test.go index 57d193f..706fe09 100644 --- a/pkg/pluginsdk/runtime/capability_servers_compat_test.go +++ b/pkg/pluginsdk/runtime/capability_servers_compat_test.go @@ -1,7 +1,10 @@ package runtime -// Compile-time guard for the released v0.12 unkeyed composite literal shape. -// New optional servers belong behind additive registration APIs, not here. +// Compile-time guard for the CapabilityServers unkeyed composite literal +// shape. v0.12 through v0.15 had twelve fields; v0.16.0 appended +// NetworkAccessProvider as the thirteenth. Plugins should use keyed literals; +// any further optional servers belong behind additive registration APIs +// (see ServeManifestOption), not here. var _ = CapabilityServers{ nil, nil, @@ -15,4 +18,5 @@ var _ = CapabilityServers{ nil, nil, nil, + nil, } diff --git a/pkg/pluginsdk/runtime/network_access_provider_test.go b/pkg/pluginsdk/runtime/network_access_provider_test.go new file mode 100644 index 0000000..c114071 --- /dev/null +++ b/pkg/pluginsdk/runtime/network_access_provider_test.go @@ -0,0 +1,44 @@ +package runtime_test + +import ( + "testing" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" + runtime "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/runtime" + "github.com/hashicorp/go-plugin" + "google.golang.org/grpc" +) + +type stubNetworkAccessProvider struct { + pluginv1.UnimplementedNetworkAccessProviderServer +} + +func TestGRPCServerRegistersNetworkAccessProvider(t *testing.T) { + plugins := runtime.DefaultPluginSet(runtime.CapabilityServers{ + Runtime: stubRuntime{}, + NetworkAccessProvider: stubNetworkAccessProvider{}, + }) + p, ok := plugins[runtime.PluginSetName].(plugin.GRPCPlugin) + if !ok { + t.Fatalf("plugin type = %T, want plugin.GRPCPlugin", plugins[runtime.PluginSetName]) + } + srv := grpc.NewServer() + if err := p.GRPCServer(nil, srv); err != nil { + t.Fatalf("GRPCServer = %v, want nil", err) + } + if _, ok := srv.GetServiceInfo()["silo.plugin.v1.NetworkAccessProvider"]; !ok { + t.Fatalf("NetworkAccessProvider service not registered; got %v", srv.GetServiceInfo()) + } +} + +func TestGRPCServerSkipsNilNetworkAccessProvider(t *testing.T) { + plugins := runtime.DefaultPluginSet(runtime.CapabilityServers{Runtime: stubRuntime{}}) + p := plugins[runtime.PluginSetName].(plugin.GRPCPlugin) + srv := grpc.NewServer() + if err := p.GRPCServer(nil, srv); err != nil { + t.Fatalf("GRPCServer = %v, want nil", err) + } + if _, ok := srv.GetServiceInfo()["silo.plugin.v1.NetworkAccessProvider"]; ok { + t.Fatalf("NetworkAccessProvider registered without a server") + } +} diff --git a/pkg/pluginsdk/runtime/runtime.go b/pkg/pluginsdk/runtime/runtime.go index 09d18b8..21ca160 100644 --- a/pkg/pluginsdk/runtime/runtime.go +++ b/pkg/pluginsdk/runtime/runtime.go @@ -35,6 +35,10 @@ type CapabilityServers struct { AuthProvider pluginv1.AuthProviderServer HttpRoutes pluginv1.HttpRoutesServer WatchSyncProvider pluginv1.WatchSyncProviderServer + // NetworkAccessProvider was added in v0.16.0. Plugins declaring + // network_access_provider.v1 register it here; the host treats them as + // resident and starts them at boot. + NetworkAccessProvider pluginv1.NetworkAccessProviderServer } // Client wraps the gRPC connection to a plugin and provides typed accessors @@ -155,6 +159,10 @@ func (c *Client) WatchSyncDeviceAuthorization() pluginv1.WatchSyncDeviceAuthoriz return pluginv1.NewWatchSyncDeviceAuthorizationServiceClient(c.conn) } +func (c *Client) NetworkAccessProvider() pluginv1.NetworkAccessProviderClient { + return pluginv1.NewNetworkAccessProviderClient(c.conn) +} + type GRPCPlugin struct { plugin.Plugin Servers CapabilityServers @@ -216,6 +224,9 @@ func (p *GRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, server *grpc.Server) if p.Servers.WatchSyncProvider != nil { pluginv1.RegisterWatchSyncProviderServer(server, p.Servers.WatchSyncProvider) } + if p.Servers.NetworkAccessProvider != nil { + pluginv1.RegisterNetworkAccessProviderServer(server, p.Servers.NetworkAccessProvider) + } return nil } diff --git a/pkg/pluginsdk/runtimehost/client_test.go b/pkg/pluginsdk/runtimehost/client_test.go index 47a92ce..caa4c24 100644 --- a/pkg/pluginsdk/runtimehost/client_test.go +++ b/pkg/pluginsdk/runtimehost/client_test.go @@ -132,6 +132,11 @@ func (f *fakeServer) CallPluginHTTP(_ context.Context, r *pluginv1.CallPluginHTT } func dial(t *testing.T, srv *fakeServer) *grpc.ClientConn { + t.Helper() + return dialServer(t, srv) +} + +func dialServer(t *testing.T, srv pluginv1.RuntimeHostServer) *grpc.ClientConn { t.Helper() lis := bufconn.Listen(1024 * 1024) g := grpc.NewServer() @@ -231,6 +236,47 @@ func TestGetHostInfo_MapsResponse(t *testing.T) { if got.PublicBaseURL != "https://silo.example" || got.PluginProxyBaseURL == "" { t.Fatalf("bad host info: %+v", got) } + if got.HostRole != "" || got.NodeID != 0 || got.IngressToken != "" || got.Listeners != nil { + t.Fatalf("pre-v0.16 host must map to zero values: %+v", got) + } + if got.Listener(runtimehost.ListenerAPI) != nil { + t.Fatal("Listener(api) on a host without listeners must be nil") + } +} + +func TestGetHostInfo_MapsNetworkAccessFields(t *testing.T) { + srv := &fakeServer{ + hostInfoResp: &pluginv1.GetHostInfoResponse{ + PublicBaseUrl: "https://silo.example", + HostRole: "proxy", + HostName: "proxy-1", + NodeId: 3, + IngressToken: "tok", + Listeners: []*pluginv1.HostListener{ + {Name: "api", Address: "127.0.0.1:8090", DefaultPort: 443}, + }, + }, + } + conn := dial(t, srv) + c := runtimehost.NewClient(conn) + + got, err := c.GetHostInfo(context.Background()) + if err != nil { + t.Fatalf("GetHostInfo: %v", err) + } + if got.HostRole != runtimehost.HostRoleProxy || got.HostName != "proxy-1" || got.NodeID != 3 || got.IngressToken != "tok" { + t.Fatalf("bad host info: %+v", got) + } + want := []runtimehost.HostListener{{Name: "api", Address: "127.0.0.1:8090", DefaultPort: 443}} + if !reflect.DeepEqual(got.Listeners, want) { + t.Fatalf("listeners = %+v, want %+v", got.Listeners, want) + } + if l := got.Listener(runtimehost.ListenerAPI); l == nil || l.Address != "127.0.0.1:8090" { + t.Fatalf("Listener(api) = %+v", l) + } + if got.Listener(runtimehost.ListenerJellyfin) != nil { + t.Fatal("Listener(jellyfin) should be nil when not reported") + } } func TestListLibraries_PassesUserID(t *testing.T) { diff --git a/pkg/pluginsdk/runtimehost/host_info.go b/pkg/pluginsdk/runtimehost/host_info.go index 71e0105..e35a99c 100644 --- a/pkg/pluginsdk/runtimehost/host_info.go +++ b/pkg/pluginsdk/runtimehost/host_info.go @@ -6,10 +6,63 @@ import ( pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" ) +// Host roles reported in HostInfo.HostRole. +const ( + HostRoleAPI = "api" + HostRoleProxy = "proxy" +) + +// Listener names reported in HostListener.Name. +const ( + ListenerAPI = "api" + ListenerJellyfin = "jellyfin" + ListenerABS = "abs" +) + +// HostInfo is the typed view of RuntimeHost.GetHostInfo. type HostInfo struct { PublicBaseURL string InternalBaseURL string PluginProxyBaseURL string + + // HostRole is "api" or "proxy". Empty on hosts that predate v0.16.0. + HostRole string + // HostName is the node name on proxies or the server name on the api host. + HostName string + // NodeID is stream_nodes.id on proxies and 0 on the api host. + NodeID int64 + // IngressToken is the per-process-start secret a network access provider + // stamps on proxied requests as X-Silo-Ingress-Token. Never log or persist + // it; call GetHostInfo again after a restart. + IngressToken string + // Listeners are the local listeners a network access provider should + // expose. Always contains "api" on hosts that implement v0.16.0. + Listeners []HostListener +} + +// HostListener is one local listener the host asks a network access provider +// to expose on the overlay network. +type HostListener struct { + // Name is "api", "jellyfin", or "abs". + Name string + // Address is the loopback dial address, host:port. + Address string + // DefaultPort is the port the plugin should expose the listener on; zero + // means provider default. + DefaultPort int +} + +// Listener returns the named listener, or nil when the host did not report it. +func (h *HostInfo) Listener(name string) *HostListener { + if h == nil { + return nil + } + for i := range h.Listeners { + if h.Listeners[i].Name == name { + return &h.Listeners[i] + } + } + return nil } func (c *Client) GetHostInfo(ctx context.Context) (*HostInfo, error) { @@ -17,9 +70,24 @@ func (c *Client) GetHostInfo(ctx context.Context) (*HostInfo, error) { if err != nil { return nil, err } - return &HostInfo{ + info := &HostInfo{ PublicBaseURL: resp.GetPublicBaseUrl(), InternalBaseURL: resp.GetInternalBaseUrl(), PluginProxyBaseURL: resp.GetPluginProxyBaseUrl(), - }, nil + HostRole: resp.GetHostRole(), + HostName: resp.GetHostName(), + NodeID: resp.GetNodeId(), + IngressToken: resp.GetIngressToken(), + } + if listeners := resp.GetListeners(); len(listeners) > 0 { + info.Listeners = make([]HostListener, 0, len(listeners)) + for _, l := range listeners { + info.Listeners = append(info.Listeners, HostListener{ + Name: l.GetName(), + Address: l.GetAddress(), + DefaultPort: int(l.GetDefaultPort()), + }) + } + } + return info, nil } diff --git a/pkg/pluginsdk/runtimehost/instance_state.go b/pkg/pluginsdk/runtimehost/instance_state.go new file mode 100644 index 0000000..c881409 --- /dev/null +++ b/pkg/pluginsdk/runtimehost/instance_state.go @@ -0,0 +1,130 @@ +package runtimehost + +import ( + "context" + "errors" + "fmt" + "time" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" +) + +// Instance state limits enforced by the host. The client checks them before +// the round trip so a plugin gets a clear error instead of a gRPC status. +const ( + MaxInstanceStateKeyBytes = 256 + MaxInstanceStateValueBytes = 256 << 10 + MaxInstanceStateKeys = 256 +) + +// ErrStateNotExist is returned by ReadState when no value is stored under the +// key. It plays the role of tailscale's ipn.ErrStateNotExist: callers compare +// with errors.Is. +var ErrStateNotExist = errors.New("runtimehost: instance state key does not exist") + +// DefaultInstanceStateTimeout bounds each InstanceStateStore round trip when +// the caller does not supply a context (ipn.StateStore has none). +const DefaultInstanceStateTimeout = 30 * time.Second + +// ReadInstanceState reads one key of the calling plugin instance's private +// state. It returns (nil, false, nil) when the key is absent. +func (c *Client) ReadInstanceState(ctx context.Context, key string) ([]byte, bool, error) { + if err := validateInstanceStateKey(key); err != nil { + return nil, false, err + } + resp, err := c.rpc.ReadInstanceState(ctx, &pluginv1.ReadInstanceStateRequest{Key: key}) + if err != nil { + return nil, false, err + } + if !resp.GetFound() { + return nil, false, nil + } + return append([]byte(nil), resp.GetValue()...), true, nil +} + +// WriteInstanceState stores value under key in the calling plugin instance's +// private state. The host encrypts the value and never returns it through any +// API. +func (c *Client) WriteInstanceState(ctx context.Context, key string, value []byte) error { + if err := validateInstanceStateKey(key); err != nil { + return err + } + if len(value) > MaxInstanceStateValueBytes { + return fmt.Errorf("runtimehost: instance state value for %q is %d bytes, limit %d", key, len(value), MaxInstanceStateValueBytes) + } + _, err := c.rpc.WriteInstanceState(ctx, &pluginv1.WriteInstanceStateRequest{Key: key, Value: value}) + return err +} + +func validateInstanceStateKey(key string) error { + if key == "" { + return fmt.Errorf("runtimehost: instance state key is required") + } + if len(key) > MaxInstanceStateKeyBytes { + return fmt.Errorf("runtimehost: instance state key is %d bytes, limit %d", len(key), MaxInstanceStateKeyBytes) + } + return nil +} + +// StateStore is the two-method shape of tailscale's ipn.StateStore, declared +// here so the SDK does not depend on tailscale. ReadState returns +// ErrStateNotExist for an absent key. +// +// ipn.StateStore takes ipn.StateKey (a named string type) rather than string, +// so Go will not accept an InstanceStateStore as an ipn.StateStore directly. +// A tsnet plugin wraps it with a few lines; see docs/network-access-provider.md. +type StateStore interface { + ReadState(key string) ([]byte, error) + WriteState(key string, value []byte) error +} + +// InstanceStateStore implements StateStore over the ReadInstanceState and +// WriteInstanceState RPCs. Every value lives in the host's encrypted +// per-instance scope (installation plus host), so each proxy node keeps its +// own node key without the plugin managing files. +type InstanceStateStore struct { + client *Client + timeout time.Duration +} + +// InstanceStateStore returns a store that scopes every round trip with +// DefaultInstanceStateTimeout. +func (c *Client) InstanceStateStore() *InstanceStateStore { + return &InstanceStateStore{client: c, timeout: DefaultInstanceStateTimeout} +} + +// WithTimeout returns a copy of the store using the given per-call timeout. +// A non-positive timeout disables the deadline. +func (s *InstanceStateStore) WithTimeout(timeout time.Duration) *InstanceStateStore { + return &InstanceStateStore{client: s.client, timeout: timeout} +} + +// ReadState returns the value stored under key or ErrStateNotExist. +func (s *InstanceStateStore) ReadState(key string) ([]byte, error) { + ctx, cancel := s.context() + defer cancel() + value, found, err := s.client.ReadInstanceState(ctx, key) + if err != nil { + return nil, err + } + if !found { + return nil, ErrStateNotExist + } + return value, nil +} + +// WriteState stores value under key. +func (s *InstanceStateStore) WriteState(key string, value []byte) error { + ctx, cancel := s.context() + defer cancel() + return s.client.WriteInstanceState(ctx, key, value) +} + +var _ StateStore = (*InstanceStateStore)(nil) + +func (s *InstanceStateStore) context() (context.Context, context.CancelFunc) { + if s.timeout <= 0 { + return context.WithCancel(context.Background()) + } + return context.WithTimeout(context.Background(), s.timeout) +} diff --git a/pkg/pluginsdk/runtimehost/instance_state_test.go b/pkg/pluginsdk/runtimehost/instance_state_test.go new file mode 100644 index 0000000..d30b2e6 --- /dev/null +++ b/pkg/pluginsdk/runtimehost/instance_state_test.go @@ -0,0 +1,140 @@ +package runtimehost_test + +import ( + "bytes" + "context" + "errors" + "strings" + "testing" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" + "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/runtimehost" +) + +type instanceStateServer struct { + pluginv1.UnimplementedRuntimeHostServer + values map[string][]byte + statuses []*pluginv1.NetworkAccessStatus +} + +func newInstanceStateServer() *instanceStateServer { + return &instanceStateServer{values: map[string][]byte{}} +} + +func (s *instanceStateServer) ReadInstanceState(_ context.Context, req *pluginv1.ReadInstanceStateRequest) (*pluginv1.ReadInstanceStateResponse, error) { + value, ok := s.values[req.GetKey()] + if !ok { + return &pluginv1.ReadInstanceStateResponse{}, nil + } + return &pluginv1.ReadInstanceStateResponse{Value: value, Found: true}, nil +} + +func (s *instanceStateServer) WriteInstanceState(_ context.Context, req *pluginv1.WriteInstanceStateRequest) (*pluginv1.WriteInstanceStateResponse, error) { + s.values[req.GetKey()] = append([]byte(nil), req.GetValue()...) + return &pluginv1.WriteInstanceStateResponse{}, nil +} + +func (s *instanceStateServer) ReportNetworkAccessStatus(_ context.Context, req *pluginv1.ReportNetworkAccessStatusRequest) (*pluginv1.ReportNetworkAccessStatusResponse, error) { + s.statuses = append(s.statuses, req.GetStatus()) + return &pluginv1.ReportNetworkAccessStatusResponse{}, nil +} + +func dialInstanceState(t *testing.T, srv *instanceStateServer) *runtimehost.Client { + t.Helper() + return runtimehost.NewClient(dialServer(t, srv)) +} + +func TestInstanceState_RoundTrip(t *testing.T) { + srv := newInstanceStateServer() + c := dialInstanceState(t, srv) + ctx := context.Background() + + _, found, err := c.ReadInstanceState(ctx, "_machinekey") + if err != nil || found { + t.Fatalf("ReadInstanceState before write = found %v, err %v; want absent", found, err) + } + if err := c.WriteInstanceState(ctx, "_machinekey", []byte("secret")); err != nil { + t.Fatalf("WriteInstanceState: %v", err) + } + value, found, err := c.ReadInstanceState(ctx, "_machinekey") + if err != nil || !found || string(value) != "secret" { + t.Fatalf("ReadInstanceState after write = %q found %v err %v", value, found, err) + } + // An empty stored value is still found. + if err := c.WriteInstanceState(ctx, "empty", nil); err != nil { + t.Fatalf("WriteInstanceState empty: %v", err) + } + if _, found, err := c.ReadInstanceState(ctx, "empty"); err != nil || !found { + t.Fatalf("empty value: found %v err %v, want found", found, err) + } +} + +func TestInstanceState_EnforcesLimitsClientSide(t *testing.T) { + srv := newInstanceStateServer() + c := dialInstanceState(t, srv) + ctx := context.Background() + + if _, _, err := c.ReadInstanceState(ctx, ""); err == nil { + t.Fatal("empty key accepted") + } + longKey := strings.Repeat("k", runtimehost.MaxInstanceStateKeyBytes+1) + if err := c.WriteInstanceState(ctx, longKey, []byte("x")); err == nil { + t.Fatal("oversized key accepted") + } + big := bytes.Repeat([]byte("v"), runtimehost.MaxInstanceStateValueBytes+1) + if err := c.WriteInstanceState(ctx, "big", big); err == nil { + t.Fatal("oversized value accepted") + } + if len(srv.values) != 0 { + t.Fatalf("rejected writes reached the host: %v", srv.values) + } + limit := bytes.Repeat([]byte("v"), runtimehost.MaxInstanceStateValueBytes) + if err := c.WriteInstanceState(ctx, "limit", limit); err != nil { + t.Fatalf("value at the limit rejected: %v", err) + } +} + +func TestInstanceStateStore_ImplementsStateStoreShape(t *testing.T) { + srv := newInstanceStateServer() + c := dialInstanceState(t, srv) + var store runtimehost.StateStore = c.InstanceStateStore() + + if _, err := store.ReadState("_profiles"); !errors.Is(err, runtimehost.ErrStateNotExist) { + t.Fatalf("ReadState missing = %v, want ErrStateNotExist", err) + } + if err := store.WriteState("_profiles", []byte(`{}`)); err != nil { + t.Fatalf("WriteState: %v", err) + } + got, err := store.ReadState("_profiles") + if err != nil || string(got) != `{}` { + t.Fatalf("ReadState = %q, %v", got, err) + } + if err := c.InstanceStateStore().WithTimeout(0).WriteState("k", []byte("v")); err != nil { + t.Fatalf("WriteState without deadline: %v", err) + } +} + +func TestReportNetworkAccessStatus(t *testing.T) { + srv := newInstanceStateServer() + c := dialInstanceState(t, srv) + ctx := context.Background() + + if err := c.ReportNetworkAccessStatus(ctx, nil); err == nil { + t.Fatal("nil status accepted") + } + if err := c.ReportNetworkAccessStatus(ctx, &pluginv1.NetworkAccessStatus{}); err == nil { + t.Fatal("status without state accepted") + } + status := &pluginv1.NetworkAccessStatus{ + State: runtimehost.NetworkAccessStateConnected, + Hostname: "silo.tail1234.ts.net", + Origin: "https://silo.tail1234.ts.net", + DesiredConnected: true, + } + if err := c.ReportNetworkAccessStatus(ctx, status); err != nil { + t.Fatalf("ReportNetworkAccessStatus: %v", err) + } + if len(srv.statuses) != 1 || srv.statuses[0].GetHostname() != "silo.tail1234.ts.net" || !srv.statuses[0].GetDesiredConnected() { + t.Fatalf("host received %v", srv.statuses) + } +} diff --git a/pkg/pluginsdk/runtimehost/network_access.go b/pkg/pluginsdk/runtimehost/network_access.go new file mode 100644 index 0000000..25207d7 --- /dev/null +++ b/pkg/pluginsdk/runtimehost/network_access.go @@ -0,0 +1,32 @@ +package runtimehost + +import ( + "context" + "fmt" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" +) + +// NetworkAccessStatus state values. The vocabulary is open; hosts tolerate +// values they do not recognize. +const ( + NetworkAccessStateDisconnected = "disconnected" + NetworkAccessStateAwaitingAuthorization = "awaiting_authorization" + NetworkAccessStateConnecting = "connecting" + NetworkAccessStateConnected = "connected" + NetworkAccessStateError = "error" +) + +// ReportNetworkAccessStatus pushes a network access status change to the +// host. Call it on every state transition so the host does not have to poll. +// The host logs state transitions only; auth_url is never logged. +func (c *Client) ReportNetworkAccessStatus(ctx context.Context, status *pluginv1.NetworkAccessStatus) error { + if status == nil { + return fmt.Errorf("runtimehost: network access status is required") + } + if status.GetState() == "" { + return fmt.Errorf("runtimehost: network access status state is required") + } + _, err := c.rpc.ReportNetworkAccessStatus(ctx, &pluginv1.ReportNetworkAccessStatusRequest{Status: status}) + return err +} diff --git a/proto/silo/plugin/v1/common.proto b/proto/silo/plugin/v1/common.proto index adf3ef0..dae0ff9 100644 --- a/proto/silo/plugin/v1/common.proto +++ b/proto/silo/plugin/v1/common.proto @@ -5,6 +5,7 @@ package silo.plugin.v1; option go_package = "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1;pluginv1"; import "google/protobuf/struct.proto"; +import "silo/plugin/v1/network_access_provider.proto"; import "silo/plugin/v1/watch_sync_provider.proto"; message SupportedPlatform { @@ -142,6 +143,11 @@ message CapabilityDescriptor { // of type "watch_sync_provider.v1". Keeping this typed lets the host build // connection and authorization UI without launching the plugin. WatchSyncProviderDescriptor watch_sync_provider = 10; + + // Typed network-access contract metadata. Only meaningful for capabilities + // of type "network_access_provider.v1". The host lists available overlay + // providers from this descriptor without launching the plugin. + NetworkAccessProviderDescriptor network_access_provider = 11; } // PluginPresentation describes a plugin for server operators and links them to diff --git a/proto/silo/plugin/v1/network_access_provider.proto b/proto/silo/plugin/v1/network_access_provider.proto new file mode 100644 index 0000000..b6a4050 --- /dev/null +++ b/proto/silo/plugin/v1/network_access_provider.proto @@ -0,0 +1,72 @@ +syntax = "proto3"; + +package silo.plugin.v1; + +option go_package = "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1;pluginv1"; + +// NetworkAccessProvider gives a Silo deployment an overlay-network identity +// (Tailscale, NetBird, ...). The plugin owns the overlay client and a reverse +// proxy in front of the host listeners it learns from RuntimeHost.GetHostInfo; +// the host owns supervision, per-instance state storage, status aggregation, +// and the admin API. Plugins declaring this capability are resident: the host +// starts them at boot, restarts them on crash, and stops them last. +service NetworkAccessProvider { + // Connect brings the overlay identity up and starts proxying. It returns the + // status reached so far; enrollment may continue in the background and be + // reported through RuntimeHost.ReportNetworkAccessStatus. + rpc Connect(NetworkAccessConnectRequest) returns (NetworkAccessStatus); + // Disconnect tears the overlay listener down and clears desired_connected. + rpc Disconnect(NetworkAccessDisconnectRequest) returns (NetworkAccessStatus); + // GetStatus returns the current status without changing it. + rpc GetStatus(NetworkAccessGetStatusRequest) returns (NetworkAccessStatus); +} + +message NetworkAccessConnectRequest {} + +message NetworkAccessDisconnectRequest {} + +message NetworkAccessGetStatusRequest {} + +// NetworkAccessStatus is the provider's view of one plugin instance. Every +// host (API server, each proxy node) runs its own instance and reports its own +// status; the host aggregates them. +message NetworkAccessStatus { + // One of: disconnected | awaiting_authorization | connecting | connected | error. + string state = 1; + // Overlay DNS name, e.g. silo.tail1234.ts.net. + string hostname = 2; + // scheme://host[:port] clients should use for the api listener. + string origin = 3; + // Overlay IP addresses assigned to this instance. + repeated string addresses = 4; + // One entry per exposed host listener (see RuntimeHost.GetHostInfo). + repeated NetworkAccessListener listeners = 5; + // Interactive enrollment URL. Only set while state is awaiting_authorization. + // Admin-only; the plugin must never log it. + string auth_url = 6; + // Human-readable failure detail when state is error. + string error = 7; + // Provider client version shown to admins, e.g. "tsnet 1.102.4". + string provider_version = 8; + // Plugin-owned intent that survives restarts. The plugin persists it in + // instance state and reconnects on start when true. + bool desired_connected = 9; +} + +// NetworkAccessListener maps one host listener onto its overlay origin. +message NetworkAccessListener { + // Listener name from RuntimeHost.GetHostInfo: api | jellyfin | abs. + string name = 1; + // Overlay origin serving that listener, scheme://host[:port]. + string origin = 2; +} + +// NetworkAccessProviderDescriptor is the typed manifest metadata for a +// network_access_provider.v1 capability. The host lists providers from it +// without launching the plugin. +message NetworkAccessProviderDescriptor { + // Stable provider slug, e.g. "tailscale", "netbird". Lowercase, path-safe. + string provider = 1; + // Operator-facing name, e.g. "Tailscale". + string display_name = 2; +} diff --git a/proto/silo/plugin/v1/runtime_host.proto b/proto/silo/plugin/v1/runtime_host.proto index 5932819..80165d7 100644 --- a/proto/silo/plugin/v1/runtime_host.proto +++ b/proto/silo/plugin/v1/runtime_host.proto @@ -6,6 +6,7 @@ option go_package = "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo import "google/protobuf/struct.proto"; import "silo/plugin/v1/common.proto"; +import "silo/plugin/v1/network_access_provider.proto"; // RuntimeHost is implemented by the silo host. Plugins dial it via the // go-plugin gRPC broker. See pkg/pluginsdk/runtimehost for a typed client. @@ -64,6 +65,21 @@ service RuntimeHost { // through the host control plane. Calls are treated as authenticated // service-to-service traffic, but never as admin traffic. rpc CallPluginHTTP(CallPluginHTTPRequest) returns (CallPluginHTTPResponse); + + // ReadInstanceState reads one key of the calling plugin instance's private + // state. The scope (installation plus host) is derived by the host and never + // supplied by the plugin. See docs/network-access-provider.md. + rpc ReadInstanceState(ReadInstanceStateRequest) returns (ReadInstanceStateResponse); + + // WriteInstanceState writes one key of the calling plugin instance's private + // state. The host stores values encrypted and never returns them through + // any API. Limits: key <= 256 bytes, value <= 256 KiB, <= 256 keys per scope. + rpc WriteInstanceState(WriteInstanceStateRequest) returns (WriteInstanceStateResponse); + + // ReportNetworkAccessStatus pushes a network_access_provider.v1 status + // change to the host so it does not have to poll. Plugins call it on every + // state transition; the host may still call GetStatus on demand. + rpc ReportNetworkAccessStatus(ReportNetworkAccessStatusRequest) returns (ReportNetworkAccessStatusResponse); } message PublishEventRequest { @@ -102,6 +118,33 @@ message GetHostInfoResponse { string public_base_url = 1; string internal_base_url = 2; string plugin_proxy_base_url = 3; + // Role of the process hosting this plugin instance: api | proxy. + string host_role = 4; + // Node name on proxies, or the server name on the api host. + string host_name = 5; + // stream_nodes.id on proxies; 0 on the api host. + int64 node_id = 6; + // Per-process-start secret that a network access provider stamps on + // proxied requests as X-Silo-Ingress-Token. Treat as a credential: keep it + // in memory, never log or persist it, and re-read GetHostInfo after a + // restart because the host rotates it on every start. + string ingress_token = 7; + // Host listeners a network access provider should expose. Always contains + // "api"; "jellyfin" and "abs" appear when those listeners are enabled. + // Proxies expose only "api". + repeated HostListener listeners = 8; +} + +// HostListener is one local listener the host asks a network access provider +// to expose on the overlay network. +message HostListener { + // Listener name: api | jellyfin | abs. + string name = 1; + // Loopback dial address for the listener, host:port. + string address = 2; + // Port the plugin should expose this listener on (e.g. 443 for api under + // HTTPS, 8096 for jellyfin, 13378 for abs). Zero means provider default. + int32 default_port = 3; } message ListLibrariesRequest { @@ -284,3 +327,30 @@ message CallPluginHTTPResponse { map headers = 2; bytes body = 3; } + +message ReadInstanceStateRequest { + // State key, <= 256 bytes. + string key = 1; +} + +message ReadInstanceStateResponse { + // Stored value; empty when found is false. + bytes value = 1; + // False when no value is stored under key in this instance's scope. + bool found = 2; +} + +message WriteInstanceStateRequest { + // State key, <= 256 bytes. + string key = 1; + // Value to store, <= 256 KiB. + bytes value = 2; +} + +message WriteInstanceStateResponse {} + +message ReportNetworkAccessStatusRequest { + NetworkAccessStatus status = 1; +} + +message ReportNetworkAccessStatusResponse {} From 2da2393c08804b84aec70a8582f1003ad9ab62d1 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:25:17 +0000 Subject: [PATCH 13/17] fix(example): retry state restoration, surface write failures, honor zero DefaultPort Review findings on the hello-network-access stub: restoreLocked marked restoration complete before the read succeeded, so a transient host error turned persisted intent into disconnected for the process lifetime; instance-state write failures were only logged, so an admin's connect or disconnect could report success while the intent was lost; and a listener with DefaultPort zero produced the unusable origin host:0 although zero means provider default. Co-Authored-By: Claude Fable 5.1 --- examples/hello-network-access/main.go | 29 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/examples/hello-network-access/main.go b/examples/hello-network-access/main.go index 25f897c..fe9f1a2 100644 --- a/examples/hello-network-access/main.go +++ b/examples/hello-network-access/main.go @@ -57,7 +57,9 @@ func (p *provider) Connect(ctx context.Context, _ *pluginv1.NetworkAccessConnect } p.connected = true p.desired = true - p.persistLocked(ctx, host) + if err := p.persistLocked(ctx, host); err != nil { + return nil, err + } status := p.statusLocked() p.reportLocked(ctx, host, status) return status, nil @@ -70,7 +72,9 @@ func (p *provider) Disconnect(ctx context.Context, _ *pluginv1.NetworkAccessDisc host := sdkruntime.Host() p.connected = false p.desired = false - p.persistLocked(ctx, host) + if err := p.persistLocked(ctx, host); err != nil { + return nil, err + } status := p.statusLocked() p.reportLocked(ctx, host, status) return status, nil @@ -94,12 +98,14 @@ func (p *provider) restoreLocked(ctx context.Context) { if host == nil { return } - p.restored = true value, found, err := host.ReadInstanceState(ctx, desiredKey) if err != nil { + // Leave restored false so the next RPC tries again; a transient + // host error must not turn persisted intent into "disconnected". p.logger.Warn("read instance state", "err", err) return } + p.restored = true if found && string(value) == "1" { p.desired = true p.connected = true @@ -109,17 +115,21 @@ func (p *provider) restoreLocked(ctx context.Context) { } } -func (p *provider) persistLocked(ctx context.Context, host *runtimehost.Client) { +// persistLocked writes desired_connected. A failure is returned to the RPC +// caller: the admin must know the intent did not survive, or the plugin would +// come back in the wrong state after its next restart. +func (p *provider) persistLocked(ctx context.Context, host *runtimehost.Client) error { if host == nil { - return + return nil } value := []byte("0") if p.desired { value = []byte("1") } if err := host.WriteInstanceState(ctx, desiredKey, value); err != nil { - p.logger.Warn("write instance state", "err", err) + return fmt.Errorf("persist desired_connected: %w", err) } + return nil } func (p *provider) reportLocked(ctx context.Context, host *runtimehost.Client, status *pluginv1.NetworkAccessStatus) { @@ -145,7 +155,12 @@ func (p *provider) statusLocked() *pluginv1.NetworkAccessStatus { status.Addresses = []string{"100.64.0.1"} if p.hostInfo != nil { for _, l := range p.hostInfo.Listeners { - origin := fmt.Sprintf("https://%s:%d", status.Hostname, l.DefaultPort) + // Zero asks for the provider default; this stub's default is + // HTTPS on 443, which the origin leaves implicit. + origin := "https://" + status.Hostname + if l.DefaultPort != 0 && l.DefaultPort != 443 { + origin = fmt.Sprintf("https://%s:%d", status.Hostname, l.DefaultPort) + } status.Listeners = append(status.Listeners, &pluginv1.NetworkAccessListener{Name: l.Name, Origin: origin}) if l.Name == runtimehost.ListenerAPI { status.Origin = origin From ef28015c76bf9f2e17019646a529904d7950ddee Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:47:48 +0000 Subject: [PATCH 14/17] fix(runtime): dial the host broker at bind time go-plugin keeps the connection info the host sends for a brokered stream for only five seconds (GRPCBroker.timeoutWait). The host sends it from AcceptAndServe just before calling BindHostBroker, but the SDK dialed the stream lazily on the first Host() call. A plugin whose first host call came later than that, which is the normal case for a resident network access provider idling until an admin connects it, found the stream expired and every Host() call returned nil for the rest of the process: Connect could not read host info or persist state and timed out. setBrokerID now dials as soon as the host binds the stream, while the window is open; runtimehost calls multiplex over that one connection as before. Observed against a running Silo stack: a provider connected within five seconds of start worked, one connected twenty seconds after start never could. Co-Authored-By: Claude Fable 5.1 --- examples/hello-network-access/manifest.json | 14 ++++++++ pkg/pluginsdk/runtime/runtime.go | 40 ++++++++++++++------- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/examples/hello-network-access/manifest.json b/examples/hello-network-access/manifest.json index 0cf0301..2d44e1d 100644 --- a/examples/hello-network-access/manifest.json +++ b/examples/hello-network-access/manifest.json @@ -3,6 +3,20 @@ "version": "0.1.0", "checksum": "__CHECKSUM__", "silo_api_version": "v1", + "supported_platforms": [ + { + "os": "linux", + "arch": "amd64" + }, + { + "os": "linux", + "arch": "arm64" + }, + { + "os": "darwin", + "arch": "arm64" + } + ], "capabilities": [ { "type": "network_access_provider.v1", diff --git a/pkg/pluginsdk/runtime/runtime.go b/pkg/pluginsdk/runtime/runtime.go index 21ca160..475136c 100644 --- a/pkg/pluginsdk/runtime/runtime.go +++ b/pkg/pluginsdk/runtime/runtime.go @@ -262,28 +262,43 @@ func (s *pluginHostState) setBroker(b *plugin.GRPCBroker) { s.mu.Unlock() } +// setBrokerID records the host-assigned stream and dials it at once. +// +// The dial cannot wait for the first Host() call: go-plugin's broker keeps +// the connection info the host sent for a stream for only five seconds +// (GRPCBroker.timeoutWait), and the host sends it from its AcceptAndServe +// just before invoking BindHostBroker. A plugin whose first host call comes +// later than that, which is the normal case for a resident plugin that idles +// until an admin connects it, would find the stream expired and every +// Host() call would return nil for the life of the process. Dialing here +// pins the connection while the window is open; runtimehost calls then +// multiplex over it. func (s *pluginHostState) setBrokerID(id uint32) { s.mu.Lock() + defer s.mu.Unlock() s.brokerID = id // new stream id → drop any cached client s.client = nil - s.mu.Unlock() + s.dialLocked() } -func (s *pluginHostState) host() *runtimehost.Client { - s.mu.Lock() - defer s.mu.Unlock() - if s.client != nil { - return s.client - } - if s.broker == nil || s.brokerID == 0 { - return nil +// dialLocked connects to the bound stream if it has not been connected yet. +// The caller holds s.mu. +func (s *pluginHostState) dialLocked() { + if s.client != nil || s.broker == nil || s.brokerID == 0 { + return } conn, err := s.broker.Dial(s.brokerID) if err != nil { - return nil + return } s.client = runtimehost.NewClient(conn) +} + +func (s *pluginHostState) host() *runtimehost.Client { + s.mu.Lock() + defer s.mu.Unlock() + s.dialLocked() return s.client } @@ -295,8 +310,9 @@ func SetHostBrokerID(id uint32) { pluginHost.setBrokerID(id) } // Host returns a runtimehost.Client connected to the silo host. Returns // nil before the host has invoked Runtime.BindHostBroker (i.e. very briefly -// during plugin startup) or if the broker dial fails. Capability handlers -// should treat nil as transient and either skip or surface a temporary error. +// during plugin startup) or if the broker dial failed at bind time. +// Capability handlers should treat nil as transient and either skip or +// surface a temporary error. // // The first successful call dials the host broker stream and caches the // *runtimehost.Client; later calls reuse the same client. From 488af9bde662bbbf117ec9b04748753cdf26a00c Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:30:51 -0400 Subject: [PATCH 15/17] feat(watchsync): add rating state and the series media type (#23) * feat(watchsync): add rating state and the series media type Watch-sync plugins had no way to import or export user ratings, and no media type for a series-level item, so series favorites, watchlist entries, and ratings could not cross the plugin boundary. Add, all appended without renumbering: - WatchSyncMediaType SERIES = 3 (series-level identity). - WatchSyncOperation SET_RATING = 10 and REMOVE_RATING = 11. - WatchSyncRemoteStateKind RATING = 5 and WatchSyncRemoteRatingState on WatchSyncRemoteState.rating = 7 (rating 1-10, rated_at, removed). - WatchSyncProviderDescriptor import_ratings = 17, export_ratings = 18. - WatchSyncEvent.rating = 15. Ratings are integers from 1 to 10; rating writes are convergent desired-state operations; an item absent from a complete RATING snapshot is unrated. The manifest validator counts the ratings flags as operations and requires MOVIE or SERIES support when either is set. Co-Authored-By: Claude Opus 5.5 (1M context) * docs: limit the mixed-version decoding claim to new SDKs Only nodes built on this SDK or later decode capability metadata tolerantly; older nodes still reject newer fields and must be upgraded before plugins publish them. Co-Authored-By: Claude Opus 5.5 (1M context) --------- Co-authored-by: Claude Opus 5.5 (1M context) --- README.md | 41 ++- docs/compatibility.md | 6 + .../silo/plugin/v1/watch_sync_provider.pb.go | 328 +++++++++++++----- .../plugin/v1/watch_sync_provider_test.go | 85 +++++ pkg/pluginsdk/convert/convert.go | 10 +- .../convert/watch_sync_provider_test.go | 49 ++- pkg/pluginsdk/manifest/manifest.go | 25 +- .../manifest/watch_sync_provider_test.go | 115 ++++++ .../silo/plugin/v1/watch_sync_provider.proto | 46 ++- 9 files changed, 595 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 9914794..ebad6e4 100644 --- a/README.md +++ b/README.md @@ -136,8 +136,9 @@ The `auth_provider.v1` capability also exposes OAuth-flow RPCs (`InitAuthorize`, watch-provider pipeline. The host owns encrypted per-profile credentials, authorization-code and device-code flow state, durable desired-state events, retries, ordering, and reconciliation. Plugins are stateless protocol adapters: -they receive secrets only for the duration of an RPC, map rich movie/episode -identity to an upstream service, and return typed apply or retry outcomes. +they receive secrets only for the duration of an RPC, map rich movie, episode, +and series identity to an upstream service, and return typed apply or retry +outcomes. Watch-sync plugins must not persist or log credentials, authorization codes, provider flow state, or secret configuration. `ApplyEvents` is an at-least-once @@ -189,17 +190,35 @@ the fault, temporary retries use `TEMPORARY`, rate limits use `RATE_LIMITED` with an optional delay, and rejected events use a non-retryable fault code. Connection-wide faults such as invalid credentials belong on the RPC response. +A `SERIES` media item describes the show itself: `external_ids`, `title`, and +`year` identify the series, and the `series_*`, season, and episode fields are +unused. + +Ratings are integers from 1 to 10 in every rating field; the host owns +conversion to its own display scale. Plugins convert between the provider's +native scale and 1–10 by rounding half up and clamping to the valid range. +`import_ratings` means `ListRemoteState` returns `RATING` states, and +`export_ratings` means `ApplyEvents` handles both `SET_RATING` and +`REMOVE_RATING`; there is no separate removal flag. `SET_RATING` carries the +value in the event's `rating` field, where zero is never valid. Both operations +are convergent desired-state writes: resending the same value, or removing a +rating that is already absent, must return `APPLIED` or `NO_CHANGE`, never a +fault. The host sends rating events only for media types listed in +`supported_media_types`, and manifest validation requires that list to include +`MOVIE` or `SERIES` when either ratings flag is set. + `ListRemoteState` returns provider-neutral typed subrecords. `watched` carries a play count and last-watched time; `progress` carries a fractional percentage and -paused time; `favorite` and `watchlist` carry list membership. An item may -contain multiple state families. The host requests only the state families a -sync phase needs, keeps that phase's `cursor` fixed while following ephemeral -page tokens, commits each successful page, and only then persists the final -`next_cursor`. `complete_snapshot=true` means the traversal is authoritative; -when false, missing items are not deletions. An incremental favorite or -watchlist removal is an item whose corresponding list state has `removed=true`; -it may omit `media` when `provider_item_key` identifies a record previously -returned to the host. When +paused time; `favorite` and `watchlist` carry list membership; `rating` carries +a 1–10 rating and when it was set. An item may contain multiple state families. +The host requests only the state families a sync phase needs, keeps that phase's +`cursor` fixed while following ephemeral page tokens, commits each successful +page, and only then persists the final `next_cursor`. `complete_snapshot=true` +means the traversal is authoritative; when false, missing items are not +deletions. In a complete `RATING` traversal, an item absent from the snapshot is +unrated. An incremental favorite, watchlist, or rating removal is an item whose +corresponding state has `removed=true`; it may omit `media` when +`provider_item_key` identifies a record previously returned to the host. When `provides_watchlist_order=true`, watchlist traversals must be complete snapshots and the order of returned watchlist states is the remote list order. Event `list_position` is presence-aware: an explicit zero means the first position, diff --git a/docs/compatibility.md b/docs/compatibility.md index 4b23b18..953996c 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -63,6 +63,11 @@ APIs), never infer it from the numeric value. Calling `GetSeasonNumber() != 0` silently conflates "no season scope" with "Specials (season zero) requested." +Watch-sync rating fields (`WatchSyncEvent.rating` and +`WatchSyncRemoteRatingState.rating`) are deliberately plain `int32`. Valid +ratings run from 1 to 10, so zero never carries a rating and no presence check +is needed. + A season-scoped `GetImagesRequest` is a scope, not a guarantee. Plugins that can filter by season should do so, and plugins should populate `ImageRecord.season_number` whenever the season is known. Hosts must bucket and @@ -73,6 +78,7 @@ verify images by the per-image field rather than assume a filtered response. - `silo_api_version` is the coarse runtime compatibility gate between Silo and a plugin binary. - Host installs should reject incompatible API versions before runtime startup. - A plugin binary should return the same manifest shape that Silo installs, except that binaries may compute their checksum dynamically at runtime. +- From this version, `convert.DecodeCapability` ignores fields and enum values it does not know, so a server node built on this SDK or later, in a mixed-version cluster sharing one database, still loads capability metadata written by a newer SDK; it only loses the parts added after its own SDK version. Nodes built on an earlier SDK decode strictly and reject such metadata, so upgrade every node past them before installing plugins that publish newer fields. ## Go Support diff --git a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider.pb.go b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider.pb.go index d561e27..e08e95f 100644 --- a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider.pb.go @@ -82,6 +82,10 @@ const ( WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_UNSPECIFIED WatchSyncMediaType = 0 WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE WatchSyncMediaType = 1 WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE WatchSyncMediaType = 2 + // A series-level item. In WatchSyncMedia, external_ids, title, and year + // describe the series itself; the series_* fields, season_number, + // episode_number, and absolute_episode_number are unused. + WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES WatchSyncMediaType = 3 ) // Enum value maps for WatchSyncMediaType. @@ -90,11 +94,13 @@ var ( 0: "WATCH_SYNC_MEDIA_TYPE_UNSPECIFIED", 1: "WATCH_SYNC_MEDIA_TYPE_MOVIE", 2: "WATCH_SYNC_MEDIA_TYPE_EPISODE", + 3: "WATCH_SYNC_MEDIA_TYPE_SERIES", } WatchSyncMediaType_value = map[string]int32{ "WATCH_SYNC_MEDIA_TYPE_UNSPECIFIED": 0, "WATCH_SYNC_MEDIA_TYPE_MOVIE": 1, "WATCH_SYNC_MEDIA_TYPE_EPISODE": 2, + "WATCH_SYNC_MEDIA_TYPE_SERIES": 3, } ) @@ -138,21 +144,27 @@ const ( WatchSyncOperation_WATCH_SYNC_OPERATION_SCROBBLE_START WatchSyncOperation = 7 WatchSyncOperation_WATCH_SYNC_OPERATION_SCROBBLE_PAUSE WatchSyncOperation = 8 WatchSyncOperation_WATCH_SYNC_OPERATION_SCROBBLE_STOP WatchSyncOperation = 9 + // Rating writes carry the desired value in WatchSyncEvent.rating. Both are + // convergent desired-state operations; see WatchSyncEvent.rating. + WatchSyncOperation_WATCH_SYNC_OPERATION_SET_RATING WatchSyncOperation = 10 + WatchSyncOperation_WATCH_SYNC_OPERATION_REMOVE_RATING WatchSyncOperation = 11 ) // Enum value maps for WatchSyncOperation. var ( WatchSyncOperation_name = map[int32]string{ - 0: "WATCH_SYNC_OPERATION_UNSPECIFIED", - 1: "WATCH_SYNC_OPERATION_MARK_WATCHED", - 2: "WATCH_SYNC_OPERATION_MARK_UNWATCHED", - 3: "WATCH_SYNC_OPERATION_ADD_FAVORITE", - 4: "WATCH_SYNC_OPERATION_REMOVE_FAVORITE", - 5: "WATCH_SYNC_OPERATION_ADD_TO_WATCHLIST", - 6: "WATCH_SYNC_OPERATION_REMOVE_FROM_WATCHLIST", - 7: "WATCH_SYNC_OPERATION_SCROBBLE_START", - 8: "WATCH_SYNC_OPERATION_SCROBBLE_PAUSE", - 9: "WATCH_SYNC_OPERATION_SCROBBLE_STOP", + 0: "WATCH_SYNC_OPERATION_UNSPECIFIED", + 1: "WATCH_SYNC_OPERATION_MARK_WATCHED", + 2: "WATCH_SYNC_OPERATION_MARK_UNWATCHED", + 3: "WATCH_SYNC_OPERATION_ADD_FAVORITE", + 4: "WATCH_SYNC_OPERATION_REMOVE_FAVORITE", + 5: "WATCH_SYNC_OPERATION_ADD_TO_WATCHLIST", + 6: "WATCH_SYNC_OPERATION_REMOVE_FROM_WATCHLIST", + 7: "WATCH_SYNC_OPERATION_SCROBBLE_START", + 8: "WATCH_SYNC_OPERATION_SCROBBLE_PAUSE", + 9: "WATCH_SYNC_OPERATION_SCROBBLE_STOP", + 10: "WATCH_SYNC_OPERATION_SET_RATING", + 11: "WATCH_SYNC_OPERATION_REMOVE_RATING", } WatchSyncOperation_value = map[string]int32{ "WATCH_SYNC_OPERATION_UNSPECIFIED": 0, @@ -165,6 +177,8 @@ var ( "WATCH_SYNC_OPERATION_SCROBBLE_START": 7, "WATCH_SYNC_OPERATION_SCROBBLE_PAUSE": 8, "WATCH_SYNC_OPERATION_SCROBBLE_STOP": 9, + "WATCH_SYNC_OPERATION_SET_RATING": 10, + "WATCH_SYNC_OPERATION_REMOVE_RATING": 11, } ) @@ -203,6 +217,7 @@ const ( WatchSyncRemoteStateKind_WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS WatchSyncRemoteStateKind = 2 WatchSyncRemoteStateKind_WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE WatchSyncRemoteStateKind = 3 WatchSyncRemoteStateKind_WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST WatchSyncRemoteStateKind = 4 + WatchSyncRemoteStateKind_WATCH_SYNC_REMOTE_STATE_KIND_RATING WatchSyncRemoteStateKind = 5 ) // Enum value maps for WatchSyncRemoteStateKind. @@ -213,6 +228,7 @@ var ( 2: "WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS", 3: "WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE", 4: "WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST", + 5: "WATCH_SYNC_REMOTE_STATE_KIND_RATING", } WatchSyncRemoteStateKind_value = map[string]int32{ "WATCH_SYNC_REMOTE_STATE_KIND_UNSPECIFIED": 0, @@ -220,6 +236,7 @@ var ( "WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS": 2, "WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE": 3, "WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST": 4, + "WATCH_SYNC_REMOTE_STATE_KIND_RATING": 5, } ) @@ -494,8 +511,19 @@ type WatchSyncProviderDescriptor struct { // an incremental subset cannot define positions relative to omitted items. ProvidesWatchlistOrder bool `protobuf:"varint,15,opt,name=provides_watchlist_order,json=providesWatchlistOrder,proto3" json:"provides_watchlist_order,omitempty"` ScrobblePlayback bool `protobuf:"varint,16,opt,name=scrobble_playback,json=scrobblePlayback,proto3" json:"scrobble_playback,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Ratings are integers from 1 to 10. Plugins convert between the provider's + // native scale and 1..10 by rounding half up and clamping to the valid range. + // The host sends rating events only for media types listed in + // supported_media_types, which must include MOVIE or SERIES when either + // ratings flag is set. + // + // When import_ratings is set, ListRemoteState returns RATING states. + ImportRatings bool `protobuf:"varint,17,opt,name=import_ratings,json=importRatings,proto3" json:"import_ratings,omitempty"` + // When export_ratings is set, ApplyEvents handles both SET_RATING and + // REMOVE_RATING; there is no separate flag for rating removal. + ExportRatings bool `protobuf:"varint,18,opt,name=export_ratings,json=exportRatings,proto3" json:"export_ratings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *WatchSyncProviderDescriptor) Reset() { @@ -640,6 +668,20 @@ func (x *WatchSyncProviderDescriptor) GetScrobblePlayback() bool { return false } +func (x *WatchSyncProviderDescriptor) GetImportRatings() bool { + if x != nil { + return x.ImportRatings + } + return false +} + +func (x *WatchSyncProviderDescriptor) GetExportRatings() bool { + if x != nil { + return x.ExportRatings + } + return false +} + type WatchSyncProviderConfig struct { state protoimpl.MessageState `protogen:"open.v1"` Values map[string]string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` @@ -1953,7 +1995,13 @@ type WatchSyncEvent struct { // Authoritative host completion state for playback events. A history ID can // exist for incomplete playback, and completion_percent may be below 100 // when the host's configured watched threshold has been reached. - Completed bool `protobuf:"varint,14,opt,name=completed,proto3" json:"completed,omitempty"` + Completed bool `protobuf:"varint,14,opt,name=completed,proto3" json:"completed,omitempty"` + // Desired rating for SET_RATING, an integer from 1 to 10. Required for + // SET_RATING and ignored for every other operation; zero is never a valid + // rating. SET_RATING and REMOVE_RATING are convergent desired-state writes: + // resending the same value, or removing a rating that is already absent, + // must yield APPLIED or NO_CHANGE, never a fault. + Rating int32 `protobuf:"varint,15,opt,name=rating,proto3" json:"rating,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2086,6 +2134,13 @@ func (x *WatchSyncEvent) GetCompleted() bool { return false } +func (x *WatchSyncEvent) GetRating() int32 { + if x != nil { + return x.Rating + } + return 0 +} + type WatchSyncApplyEventsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Context *WatchSyncAuthenticatedContext `protobuf:"bytes,1,opt,name=context,proto3" json:"context,omitempty"` @@ -2517,11 +2572,77 @@ func (x *WatchSyncRemoteListState) GetRemoved() bool { return false } +type WatchSyncRemoteRatingState struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Integer rating from 1 to 10, converted from the provider's native scale. + // Required unless removed is true. + Rating int32 `protobuf:"varint,1,opt,name=rating,proto3" json:"rating,omitempty"` + // Provider timestamp for when the rating was last set, when available. + RatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=rated_at,json=ratedAt,proto3" json:"rated_at,omitempty"` + // True is an explicit tombstone for a rating cleared upstream. It is + // meaningful for incremental traversals, where omitting an item is not a + // deletion. + Removed bool `protobuf:"varint,3,opt,name=removed,proto3" json:"removed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WatchSyncRemoteRatingState) Reset() { + *x = WatchSyncRemoteRatingState{} + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WatchSyncRemoteRatingState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchSyncRemoteRatingState) ProtoMessage() {} + +func (x *WatchSyncRemoteRatingState) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchSyncRemoteRatingState.ProtoReflect.Descriptor instead. +func (*WatchSyncRemoteRatingState) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{27} +} + +func (x *WatchSyncRemoteRatingState) GetRating() int32 { + if x != nil { + return x.Rating + } + return 0 +} + +func (x *WatchSyncRemoteRatingState) GetRatedAt() *timestamppb.Timestamp { + if x != nil { + return x.RatedAt + } + return nil +} + +func (x *WatchSyncRemoteRatingState) GetRemoved() bool { + if x != nil { + return x.Removed + } + return false +} + type WatchSyncRemoteState struct { state protoimpl.MessageState `protogen:"open.v1"` ProviderItemKey string `protobuf:"bytes,1,opt,name=provider_item_key,json=providerItemKey,proto3" json:"provider_item_key,omitempty"` - // Required except on an explicit favorite/watchlist tombstone whose - // provider_item_key identifies a record returned previously. + // Required except on an explicit favorite, watchlist, or rating tombstone + // whose provider_item_key identifies a record returned previously. Media *WatchSyncMedia `protobuf:"bytes,2,opt,name=media,proto3" json:"media,omitempty"` // At least one typed state must be present. Both may be present when the // provider reports a completed play and a separate resume point. @@ -2529,13 +2650,14 @@ type WatchSyncRemoteState struct { Progress *WatchSyncRemoteProgressState `protobuf:"bytes,4,opt,name=progress,proto3" json:"progress,omitempty"` Favorite *WatchSyncRemoteListState `protobuf:"bytes,5,opt,name=favorite,proto3" json:"favorite,omitempty"` Watchlist *WatchSyncRemoteListState `protobuf:"bytes,6,opt,name=watchlist,proto3" json:"watchlist,omitempty"` + Rating *WatchSyncRemoteRatingState `protobuf:"bytes,7,opt,name=rating,proto3" json:"rating,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *WatchSyncRemoteState) Reset() { *x = WatchSyncRemoteState{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[27] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2547,7 +2669,7 @@ func (x *WatchSyncRemoteState) String() string { func (*WatchSyncRemoteState) ProtoMessage() {} func (x *WatchSyncRemoteState) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[27] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2560,7 +2682,7 @@ func (x *WatchSyncRemoteState) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncRemoteState.ProtoReflect.Descriptor instead. func (*WatchSyncRemoteState) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{27} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{28} } func (x *WatchSyncRemoteState) GetProviderItemKey() string { @@ -2605,6 +2727,13 @@ func (x *WatchSyncRemoteState) GetWatchlist() *WatchSyncRemoteListState { return nil } +func (x *WatchSyncRemoteState) GetRating() *WatchSyncRemoteRatingState { + if x != nil { + return x.Rating + } + return nil +} + type WatchSyncListRemoteStateResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Items []*WatchSyncRemoteState `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` @@ -2618,7 +2747,8 @@ type WatchSyncListRemoteStateResponse struct { // incremental delta, so an absent item must not be interpreted as deletion. // The value must remain stable across every page in one traversal. A provider // advertising provides_watchlist_order must set this true for WATCHLIST - // traversals so the returned order is unambiguous. + // traversals so the returned order is unambiguous. In a complete RATING + // traversal, an item absent from the snapshot is unrated. CompleteSnapshot bool `protobuf:"varint,4,opt,name=complete_snapshot,json=completeSnapshot,proto3" json:"complete_snapshot,omitempty"` // Complete authoritative replacement. The host persists it before consuming // this page or fault; a persistence failure discards the page and cursor. @@ -2633,7 +2763,7 @@ type WatchSyncListRemoteStateResponse struct { func (x *WatchSyncListRemoteStateResponse) Reset() { *x = WatchSyncListRemoteStateResponse{} - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[28] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2645,7 +2775,7 @@ func (x *WatchSyncListRemoteStateResponse) String() string { func (*WatchSyncListRemoteStateResponse) ProtoMessage() {} func (x *WatchSyncListRemoteStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[28] + mi := &file_silo_plugin_v1_watch_sync_provider_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2658,7 +2788,7 @@ func (x *WatchSyncListRemoteStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSyncListRemoteStateResponse.ProtoReflect.Descriptor instead. func (*WatchSyncListRemoteStateResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{28} + return file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP(), []int{29} } func (x *WatchSyncListRemoteStateResponse) GetItems() []*WatchSyncRemoteState { @@ -2707,7 +2837,7 @@ var File_silo_plugin_v1_watch_sync_provider_proto protoreflect.FileDescriptor const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\n" + - "(silo/plugin/v1/watch_sync_provider.proto\x12\x0esilo.plugin.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xa4\x06\n" + + "(silo/plugin/v1/watch_sync_provider.proto\x12\x0esilo.plugin.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xf2\x06\n" + "\x1bWatchSyncProviderDescriptor\x12F\n" + "\fauth_methods\x18\x01 \x03(\x0e2#.silo.plugin.v1.WatchSyncAuthMethodR\vauthMethods\x12%\n" + "\x0eexport_watched\x18\x02 \x01(\bR\rexportWatched\x12)\n" + @@ -2725,7 +2855,9 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\x10export_watchlist\x18\r \x01(\bR\x0fexportWatchlist\x12)\n" + "\x10remove_watchlist\x18\x0e \x01(\bR\x0fremoveWatchlist\x128\n" + "\x18provides_watchlist_order\x18\x0f \x01(\bR\x16providesWatchlistOrder\x12+\n" + - "\x11scrobble_playback\x18\x10 \x01(\bR\x10scrobblePlayback\"\xc2\x02\n" + + "\x11scrobble_playback\x18\x10 \x01(\bR\x10scrobblePlayback\x12%\n" + + "\x0eimport_ratings\x18\x11 \x01(\bR\rimportRatings\x12%\n" + + "\x0eexport_ratings\x18\x12 \x01(\bR\rexportRatings\"\xc2\x02\n" + "\x17WatchSyncProviderConfig\x12K\n" + "\x06values\x18\x01 \x03(\v23.silo.plugin.v1.WatchSyncProviderConfig.ValuesEntryR\x06values\x12^\n" + "\rsecret_values\x18\x02 \x03(\v29.silo.plugin.v1.WatchSyncProviderConfig.SecretValuesEntryR\fsecretValues\x1a9\n" + @@ -2844,7 +2976,7 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aD\n" + "\x16SeriesExternalIdsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x9d\x05\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xb5\x05\n" + "\x0eWatchSyncEvent\x12\x19\n" + "\bevent_id\x18\x01 \x01(\tR\aeventId\x12@\n" + "\toperation\x18\x02 \x01(\x0e2\".silo.plugin.v1.WatchSyncOperationR\toperation\x127\n" + @@ -2862,7 +2994,8 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "play_count\x18\v \x01(\x05R\tplayCount\x12(\n" + "\rlist_position\x18\f \x01(\x05H\x00R\flistPosition\x88\x01\x01\x12*\n" + "\x11provider_item_key\x18\r \x01(\tR\x0fproviderItemKey\x12\x1c\n" + - "\tcompleted\x18\x0e \x01(\bR\tcompletedB\x10\n" + + "\tcompleted\x18\x0e \x01(\bR\tcompleted\x12\x16\n" + + "\x06rating\x18\x0f \x01(\x05R\x06ratingB\x10\n" + "\x0e_list_position\"\x9e\x01\n" + "\x1bWatchSyncApplyEventsRequest\x12G\n" + "\acontext\x18\x01 \x01(\v2-.silo.plugin.v1.WatchSyncAuthenticatedContextR\acontext\x126\n" + @@ -2892,14 +3025,19 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\tpaused_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\bpausedAt\"m\n" + "\x18WatchSyncRemoteListState\x127\n" + "\tlisted_at\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\blistedAt\x12\x18\n" + - "\aremoved\x18\x02 \x01(\bR\aremoved\"\x97\x03\n" + + "\aremoved\x18\x02 \x01(\bR\aremoved\"\x85\x01\n" + + "\x1aWatchSyncRemoteRatingState\x12\x16\n" + + "\x06rating\x18\x01 \x01(\x05R\x06rating\x125\n" + + "\brated_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\aratedAt\x12\x18\n" + + "\aremoved\x18\x03 \x01(\bR\aremoved\"\xdb\x03\n" + "\x14WatchSyncRemoteState\x12*\n" + "\x11provider_item_key\x18\x01 \x01(\tR\x0fproviderItemKey\x124\n" + "\x05media\x18\x02 \x01(\v2\x1e.silo.plugin.v1.WatchSyncMediaR\x05media\x12E\n" + "\awatched\x18\x03 \x01(\v2+.silo.plugin.v1.WatchSyncRemoteWatchedStateR\awatched\x12H\n" + "\bprogress\x18\x04 \x01(\v2,.silo.plugin.v1.WatchSyncRemoteProgressStateR\bprogress\x12D\n" + "\bfavorite\x18\x05 \x01(\v2(.silo.plugin.v1.WatchSyncRemoteListStateR\bfavorite\x12F\n" + - "\twatchlist\x18\x06 \x01(\v2(.silo.plugin.v1.WatchSyncRemoteListStateR\twatchlist\"\xe1\x02\n" + + "\twatchlist\x18\x06 \x01(\v2(.silo.plugin.v1.WatchSyncRemoteListStateR\twatchlist\x12B\n" + + "\x06rating\x18\a \x01(\v2*.silo.plugin.v1.WatchSyncRemoteRatingStateR\x06rating\"\xe1\x02\n" + " WatchSyncListRemoteStateResponse\x12:\n" + "\x05items\x18\x01 \x03(\v2$.silo.plugin.v1.WatchSyncRemoteStateR\x05items\x12&\n" + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1f\n" + @@ -2912,11 +3050,12 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "\"WATCH_SYNC_AUTH_METHOD_UNSPECIFIED\x10\x00\x12-\n" + ")WATCH_SYNC_AUTH_METHOD_AUTHORIZATION_CODE\x10\x01\x12\"\n" + "\x1eWATCH_SYNC_AUTH_METHOD_API_KEY\x10\x02\x12&\n" + - "\"WATCH_SYNC_AUTH_METHOD_DEVICE_CODE\x10\x03*\x7f\n" + + "\"WATCH_SYNC_AUTH_METHOD_DEVICE_CODE\x10\x03*\xa1\x01\n" + "\x12WatchSyncMediaType\x12%\n" + "!WATCH_SYNC_MEDIA_TYPE_UNSPECIFIED\x10\x00\x12\x1f\n" + "\x1bWATCH_SYNC_MEDIA_TYPE_MOVIE\x10\x01\x12!\n" + - "\x1dWATCH_SYNC_MEDIA_TYPE_EPISODE\x10\x02*\xb0\x03\n" + + "\x1dWATCH_SYNC_MEDIA_TYPE_EPISODE\x10\x02\x12 \n" + + "\x1cWATCH_SYNC_MEDIA_TYPE_SERIES\x10\x03*\xfd\x03\n" + "\x12WatchSyncOperation\x12$\n" + " WATCH_SYNC_OPERATION_UNSPECIFIED\x10\x00\x12%\n" + "!WATCH_SYNC_OPERATION_MARK_WATCHED\x10\x01\x12'\n" + @@ -2927,13 +3066,17 @@ const file_silo_plugin_v1_watch_sync_provider_proto_rawDesc = "" + "*WATCH_SYNC_OPERATION_REMOVE_FROM_WATCHLIST\x10\x06\x12'\n" + "#WATCH_SYNC_OPERATION_SCROBBLE_START\x10\a\x12'\n" + "#WATCH_SYNC_OPERATION_SCROBBLE_PAUSE\x10\b\x12&\n" + - "\"WATCH_SYNC_OPERATION_SCROBBLE_STOP\x10\t*\xf4\x01\n" + + "\"WATCH_SYNC_OPERATION_SCROBBLE_STOP\x10\t\x12#\n" + + "\x1fWATCH_SYNC_OPERATION_SET_RATING\x10\n" + + "\x12&\n" + + "\"WATCH_SYNC_OPERATION_REMOVE_RATING\x10\v*\x9d\x02\n" + "\x18WatchSyncRemoteStateKind\x12,\n" + "(WATCH_SYNC_REMOTE_STATE_KIND_UNSPECIFIED\x10\x00\x12(\n" + "$WATCH_SYNC_REMOTE_STATE_KIND_WATCHED\x10\x01\x12)\n" + "%WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS\x10\x02\x12)\n" + "%WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE\x10\x03\x12*\n" + - "&WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST\x10\x04*\xae\x02\n" + + "&WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST\x10\x04\x12'\n" + + "#WATCH_SYNC_REMOTE_STATE_KIND_RATING\x10\x05*\xae\x02\n" + "\"WatchSyncDeviceAuthorizationStatus\x126\n" + "2WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_UNSPECIFIED\x10\x00\x122\n" + ".WATCH_SYNC_DEVICE_AUTHORIZATION_STATUS_PENDING\x10\x01\x125\n" + @@ -2985,7 +3128,7 @@ func file_silo_plugin_v1_watch_sync_provider_proto_rawDescGZIP() []byte { } var file_silo_plugin_v1_watch_sync_provider_proto_enumTypes = make([]protoimpl.EnumInfo, 8) -var file_silo_plugin_v1_watch_sync_provider_proto_msgTypes = make([]protoimpl.MessageInfo, 34) +var file_silo_plugin_v1_watch_sync_provider_proto_msgTypes = make([]protoimpl.MessageInfo, 35) var file_silo_plugin_v1_watch_sync_provider_proto_goTypes = []any{ (WatchSyncAuthMethod)(0), // 0: silo.plugin.v1.WatchSyncAuthMethod (WatchSyncMediaType)(0), // 1: silo.plugin.v1.WatchSyncMediaType @@ -3022,43 +3165,44 @@ var file_silo_plugin_v1_watch_sync_provider_proto_goTypes = []any{ (*WatchSyncRemoteWatchedState)(nil), // 32: silo.plugin.v1.WatchSyncRemoteWatchedState (*WatchSyncRemoteProgressState)(nil), // 33: silo.plugin.v1.WatchSyncRemoteProgressState (*WatchSyncRemoteListState)(nil), // 34: silo.plugin.v1.WatchSyncRemoteListState - (*WatchSyncRemoteState)(nil), // 35: silo.plugin.v1.WatchSyncRemoteState - (*WatchSyncListRemoteStateResponse)(nil), // 36: silo.plugin.v1.WatchSyncListRemoteStateResponse - nil, // 37: silo.plugin.v1.WatchSyncProviderConfig.ValuesEntry - nil, // 38: silo.plugin.v1.WatchSyncProviderConfig.SecretValuesEntry - nil, // 39: silo.plugin.v1.WatchSyncCredentials.SecretAttributesEntry - nil, // 40: silo.plugin.v1.WatchSyncMedia.ExternalIdsEntry - nil, // 41: silo.plugin.v1.WatchSyncMedia.SeriesExternalIdsEntry - (*timestamppb.Timestamp)(nil), // 42: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 43: google.protobuf.Duration - (*structpb.Struct)(nil), // 44: google.protobuf.Struct + (*WatchSyncRemoteRatingState)(nil), // 35: silo.plugin.v1.WatchSyncRemoteRatingState + (*WatchSyncRemoteState)(nil), // 36: silo.plugin.v1.WatchSyncRemoteState + (*WatchSyncListRemoteStateResponse)(nil), // 37: silo.plugin.v1.WatchSyncListRemoteStateResponse + nil, // 38: silo.plugin.v1.WatchSyncProviderConfig.ValuesEntry + nil, // 39: silo.plugin.v1.WatchSyncProviderConfig.SecretValuesEntry + nil, // 40: silo.plugin.v1.WatchSyncCredentials.SecretAttributesEntry + nil, // 41: silo.plugin.v1.WatchSyncMedia.ExternalIdsEntry + nil, // 42: silo.plugin.v1.WatchSyncMedia.SeriesExternalIdsEntry + (*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 44: google.protobuf.Duration + (*structpb.Struct)(nil), // 45: google.protobuf.Struct } var file_silo_plugin_v1_watch_sync_provider_proto_depIdxs = []int32{ 0, // 0: silo.plugin.v1.WatchSyncProviderDescriptor.auth_methods:type_name -> silo.plugin.v1.WatchSyncAuthMethod 1, // 1: silo.plugin.v1.WatchSyncProviderDescriptor.supported_media_types:type_name -> silo.plugin.v1.WatchSyncMediaType - 37, // 2: silo.plugin.v1.WatchSyncProviderConfig.values:type_name -> silo.plugin.v1.WatchSyncProviderConfig.ValuesEntry - 38, // 3: silo.plugin.v1.WatchSyncProviderConfig.secret_values:type_name -> silo.plugin.v1.WatchSyncProviderConfig.SecretValuesEntry - 42, // 4: silo.plugin.v1.WatchSyncCredentials.expires_at:type_name -> google.protobuf.Timestamp - 39, // 5: silo.plugin.v1.WatchSyncCredentials.secret_attributes:type_name -> silo.plugin.v1.WatchSyncCredentials.SecretAttributesEntry + 38, // 2: silo.plugin.v1.WatchSyncProviderConfig.values:type_name -> silo.plugin.v1.WatchSyncProviderConfig.ValuesEntry + 39, // 3: silo.plugin.v1.WatchSyncProviderConfig.secret_values:type_name -> silo.plugin.v1.WatchSyncProviderConfig.SecretValuesEntry + 43, // 4: silo.plugin.v1.WatchSyncCredentials.expires_at:type_name -> google.protobuf.Timestamp + 40, // 5: silo.plugin.v1.WatchSyncCredentials.secret_attributes:type_name -> silo.plugin.v1.WatchSyncCredentials.SecretAttributesEntry 9, // 6: silo.plugin.v1.WatchSyncAuthenticatedContext.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig 10, // 7: silo.plugin.v1.WatchSyncAuthenticatedContext.credentials:type_name -> silo.plugin.v1.WatchSyncCredentials 7, // 8: silo.plugin.v1.WatchSyncFault.code:type_name -> silo.plugin.v1.WatchSyncFaultCode - 43, // 9: silo.plugin.v1.WatchSyncFault.retry_after:type_name -> google.protobuf.Duration + 44, // 9: silo.plugin.v1.WatchSyncFault.retry_after:type_name -> google.protobuf.Duration 9, // 10: silo.plugin.v1.WatchSyncInitAuthorizeRequest.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig 13, // 11: silo.plugin.v1.WatchSyncInitAuthorizeResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault 9, // 12: silo.plugin.v1.WatchSyncExchangeCodeRequest.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig 9, // 13: silo.plugin.v1.WatchSyncExchangeAPIKeyRequest.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig 9, // 14: silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartRequest.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig - 43, // 15: silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse.polling_interval:type_name -> google.protobuf.Duration - 42, // 16: silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse.expires_at:type_name -> google.protobuf.Timestamp + 44, // 15: silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse.polling_interval:type_name -> google.protobuf.Duration + 43, // 16: silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse.expires_at:type_name -> google.protobuf.Timestamp 13, // 17: silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault 9, // 18: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollRequest.provider_config:type_name -> silo.plugin.v1.WatchSyncProviderConfig 4, // 19: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.status:type_name -> silo.plugin.v1.WatchSyncDeviceAuthorizationStatus 10, // 20: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.credentials:type_name -> silo.plugin.v1.WatchSyncCredentials 12, // 21: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.account:type_name -> silo.plugin.v1.WatchSyncAccount 13, // 22: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault - 43, // 23: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.polling_interval:type_name -> google.protobuf.Duration - 42, // 24: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.expires_at:type_name -> google.protobuf.Timestamp + 44, // 23: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.polling_interval:type_name -> google.protobuf.Duration + 43, // 24: silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse.expires_at:type_name -> google.protobuf.Timestamp 11, // 25: silo.plugin.v1.WatchSyncRefreshCredentialsRequest.context:type_name -> silo.plugin.v1.WatchSyncAuthenticatedContext 10, // 26: silo.plugin.v1.WatchSyncCredentialResponse.credentials:type_name -> silo.plugin.v1.WatchSyncCredentials 12, // 27: silo.plugin.v1.WatchSyncCredentialResponse.account:type_name -> silo.plugin.v1.WatchSyncAccount @@ -3067,12 +3211,12 @@ var file_silo_plugin_v1_watch_sync_provider_proto_depIdxs = []int32{ 12, // 30: silo.plugin.v1.WatchSyncGetAccountResponse.account:type_name -> silo.plugin.v1.WatchSyncAccount 13, // 31: silo.plugin.v1.WatchSyncGetAccountResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault 1, // 32: silo.plugin.v1.WatchSyncMedia.media_type:type_name -> silo.plugin.v1.WatchSyncMediaType - 40, // 33: silo.plugin.v1.WatchSyncMedia.external_ids:type_name -> silo.plugin.v1.WatchSyncMedia.ExternalIdsEntry - 41, // 34: silo.plugin.v1.WatchSyncMedia.series_external_ids:type_name -> silo.plugin.v1.WatchSyncMedia.SeriesExternalIdsEntry - 44, // 35: silo.plugin.v1.WatchSyncMedia.metadata:type_name -> google.protobuf.Struct + 41, // 33: silo.plugin.v1.WatchSyncMedia.external_ids:type_name -> silo.plugin.v1.WatchSyncMedia.ExternalIdsEntry + 42, // 34: silo.plugin.v1.WatchSyncMedia.series_external_ids:type_name -> silo.plugin.v1.WatchSyncMedia.SeriesExternalIdsEntry + 45, // 35: silo.plugin.v1.WatchSyncMedia.metadata:type_name -> google.protobuf.Struct 2, // 36: silo.plugin.v1.WatchSyncEvent.operation:type_name -> silo.plugin.v1.WatchSyncOperation 5, // 37: silo.plugin.v1.WatchSyncEvent.origin:type_name -> silo.plugin.v1.WatchSyncOrigin - 42, // 38: silo.plugin.v1.WatchSyncEvent.occurred_at:type_name -> google.protobuf.Timestamp + 43, // 38: silo.plugin.v1.WatchSyncEvent.occurred_at:type_name -> google.protobuf.Timestamp 26, // 39: silo.plugin.v1.WatchSyncEvent.media:type_name -> silo.plugin.v1.WatchSyncMedia 11, // 40: silo.plugin.v1.WatchSyncApplyEventsRequest.context:type_name -> silo.plugin.v1.WatchSyncAuthenticatedContext 27, // 41: silo.plugin.v1.WatchSyncApplyEventsRequest.events:type_name -> silo.plugin.v1.WatchSyncEvent @@ -3083,40 +3227,42 @@ var file_silo_plugin_v1_watch_sync_provider_proto_depIdxs = []int32{ 13, // 46: silo.plugin.v1.WatchSyncApplyEventsResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault 11, // 47: silo.plugin.v1.WatchSyncListRemoteStateRequest.context:type_name -> silo.plugin.v1.WatchSyncAuthenticatedContext 3, // 48: silo.plugin.v1.WatchSyncListRemoteStateRequest.state_kinds:type_name -> silo.plugin.v1.WatchSyncRemoteStateKind - 42, // 49: silo.plugin.v1.WatchSyncRemoteWatchedState.last_watched_at:type_name -> google.protobuf.Timestamp - 42, // 50: silo.plugin.v1.WatchSyncRemoteProgressState.paused_at:type_name -> google.protobuf.Timestamp - 42, // 51: silo.plugin.v1.WatchSyncRemoteListState.listed_at:type_name -> google.protobuf.Timestamp - 26, // 52: silo.plugin.v1.WatchSyncRemoteState.media:type_name -> silo.plugin.v1.WatchSyncMedia - 32, // 53: silo.plugin.v1.WatchSyncRemoteState.watched:type_name -> silo.plugin.v1.WatchSyncRemoteWatchedState - 33, // 54: silo.plugin.v1.WatchSyncRemoteState.progress:type_name -> silo.plugin.v1.WatchSyncRemoteProgressState - 34, // 55: silo.plugin.v1.WatchSyncRemoteState.favorite:type_name -> silo.plugin.v1.WatchSyncRemoteListState - 34, // 56: silo.plugin.v1.WatchSyncRemoteState.watchlist:type_name -> silo.plugin.v1.WatchSyncRemoteListState - 35, // 57: silo.plugin.v1.WatchSyncListRemoteStateResponse.items:type_name -> silo.plugin.v1.WatchSyncRemoteState - 10, // 58: silo.plugin.v1.WatchSyncListRemoteStateResponse.updated_credentials:type_name -> silo.plugin.v1.WatchSyncCredentials - 13, // 59: silo.plugin.v1.WatchSyncListRemoteStateResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault - 14, // 60: silo.plugin.v1.WatchSyncProvider.InitAuthorize:input_type -> silo.plugin.v1.WatchSyncInitAuthorizeRequest - 16, // 61: silo.plugin.v1.WatchSyncProvider.ExchangeCode:input_type -> silo.plugin.v1.WatchSyncExchangeCodeRequest - 17, // 62: silo.plugin.v1.WatchSyncProvider.ExchangeAPIKey:input_type -> silo.plugin.v1.WatchSyncExchangeAPIKeyRequest - 22, // 63: silo.plugin.v1.WatchSyncProvider.RefreshCredentials:input_type -> silo.plugin.v1.WatchSyncRefreshCredentialsRequest - 24, // 64: silo.plugin.v1.WatchSyncProvider.GetAccount:input_type -> silo.plugin.v1.WatchSyncGetAccountRequest - 28, // 65: silo.plugin.v1.WatchSyncProvider.ApplyEvents:input_type -> silo.plugin.v1.WatchSyncApplyEventsRequest - 31, // 66: silo.plugin.v1.WatchSyncProvider.ListRemoteState:input_type -> silo.plugin.v1.WatchSyncListRemoteStateRequest - 18, // 67: silo.plugin.v1.WatchSyncDeviceAuthorizationService.Start:input_type -> silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartRequest - 20, // 68: silo.plugin.v1.WatchSyncDeviceAuthorizationService.Poll:input_type -> silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollRequest - 15, // 69: silo.plugin.v1.WatchSyncProvider.InitAuthorize:output_type -> silo.plugin.v1.WatchSyncInitAuthorizeResponse - 23, // 70: silo.plugin.v1.WatchSyncProvider.ExchangeCode:output_type -> silo.plugin.v1.WatchSyncCredentialResponse - 23, // 71: silo.plugin.v1.WatchSyncProvider.ExchangeAPIKey:output_type -> silo.plugin.v1.WatchSyncCredentialResponse - 23, // 72: silo.plugin.v1.WatchSyncProvider.RefreshCredentials:output_type -> silo.plugin.v1.WatchSyncCredentialResponse - 25, // 73: silo.plugin.v1.WatchSyncProvider.GetAccount:output_type -> silo.plugin.v1.WatchSyncGetAccountResponse - 30, // 74: silo.plugin.v1.WatchSyncProvider.ApplyEvents:output_type -> silo.plugin.v1.WatchSyncApplyEventsResponse - 36, // 75: silo.plugin.v1.WatchSyncProvider.ListRemoteState:output_type -> silo.plugin.v1.WatchSyncListRemoteStateResponse - 19, // 76: silo.plugin.v1.WatchSyncDeviceAuthorizationService.Start:output_type -> silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse - 21, // 77: silo.plugin.v1.WatchSyncDeviceAuthorizationService.Poll:output_type -> silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse - 69, // [69:78] is the sub-list for method output_type - 60, // [60:69] is the sub-list for method input_type - 60, // [60:60] is the sub-list for extension type_name - 60, // [60:60] is the sub-list for extension extendee - 0, // [0:60] is the sub-list for field type_name + 43, // 49: silo.plugin.v1.WatchSyncRemoteWatchedState.last_watched_at:type_name -> google.protobuf.Timestamp + 43, // 50: silo.plugin.v1.WatchSyncRemoteProgressState.paused_at:type_name -> google.protobuf.Timestamp + 43, // 51: silo.plugin.v1.WatchSyncRemoteListState.listed_at:type_name -> google.protobuf.Timestamp + 43, // 52: silo.plugin.v1.WatchSyncRemoteRatingState.rated_at:type_name -> google.protobuf.Timestamp + 26, // 53: silo.plugin.v1.WatchSyncRemoteState.media:type_name -> silo.plugin.v1.WatchSyncMedia + 32, // 54: silo.plugin.v1.WatchSyncRemoteState.watched:type_name -> silo.plugin.v1.WatchSyncRemoteWatchedState + 33, // 55: silo.plugin.v1.WatchSyncRemoteState.progress:type_name -> silo.plugin.v1.WatchSyncRemoteProgressState + 34, // 56: silo.plugin.v1.WatchSyncRemoteState.favorite:type_name -> silo.plugin.v1.WatchSyncRemoteListState + 34, // 57: silo.plugin.v1.WatchSyncRemoteState.watchlist:type_name -> silo.plugin.v1.WatchSyncRemoteListState + 35, // 58: silo.plugin.v1.WatchSyncRemoteState.rating:type_name -> silo.plugin.v1.WatchSyncRemoteRatingState + 36, // 59: silo.plugin.v1.WatchSyncListRemoteStateResponse.items:type_name -> silo.plugin.v1.WatchSyncRemoteState + 10, // 60: silo.plugin.v1.WatchSyncListRemoteStateResponse.updated_credentials:type_name -> silo.plugin.v1.WatchSyncCredentials + 13, // 61: silo.plugin.v1.WatchSyncListRemoteStateResponse.fault:type_name -> silo.plugin.v1.WatchSyncFault + 14, // 62: silo.plugin.v1.WatchSyncProvider.InitAuthorize:input_type -> silo.plugin.v1.WatchSyncInitAuthorizeRequest + 16, // 63: silo.plugin.v1.WatchSyncProvider.ExchangeCode:input_type -> silo.plugin.v1.WatchSyncExchangeCodeRequest + 17, // 64: silo.plugin.v1.WatchSyncProvider.ExchangeAPIKey:input_type -> silo.plugin.v1.WatchSyncExchangeAPIKeyRequest + 22, // 65: silo.plugin.v1.WatchSyncProvider.RefreshCredentials:input_type -> silo.plugin.v1.WatchSyncRefreshCredentialsRequest + 24, // 66: silo.plugin.v1.WatchSyncProvider.GetAccount:input_type -> silo.plugin.v1.WatchSyncGetAccountRequest + 28, // 67: silo.plugin.v1.WatchSyncProvider.ApplyEvents:input_type -> silo.plugin.v1.WatchSyncApplyEventsRequest + 31, // 68: silo.plugin.v1.WatchSyncProvider.ListRemoteState:input_type -> silo.plugin.v1.WatchSyncListRemoteStateRequest + 18, // 69: silo.plugin.v1.WatchSyncDeviceAuthorizationService.Start:input_type -> silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartRequest + 20, // 70: silo.plugin.v1.WatchSyncDeviceAuthorizationService.Poll:input_type -> silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollRequest + 15, // 71: silo.plugin.v1.WatchSyncProvider.InitAuthorize:output_type -> silo.plugin.v1.WatchSyncInitAuthorizeResponse + 23, // 72: silo.plugin.v1.WatchSyncProvider.ExchangeCode:output_type -> silo.plugin.v1.WatchSyncCredentialResponse + 23, // 73: silo.plugin.v1.WatchSyncProvider.ExchangeAPIKey:output_type -> silo.plugin.v1.WatchSyncCredentialResponse + 23, // 74: silo.plugin.v1.WatchSyncProvider.RefreshCredentials:output_type -> silo.plugin.v1.WatchSyncCredentialResponse + 25, // 75: silo.plugin.v1.WatchSyncProvider.GetAccount:output_type -> silo.plugin.v1.WatchSyncGetAccountResponse + 30, // 76: silo.plugin.v1.WatchSyncProvider.ApplyEvents:output_type -> silo.plugin.v1.WatchSyncApplyEventsResponse + 37, // 77: silo.plugin.v1.WatchSyncProvider.ListRemoteState:output_type -> silo.plugin.v1.WatchSyncListRemoteStateResponse + 19, // 78: silo.plugin.v1.WatchSyncDeviceAuthorizationService.Start:output_type -> silo.plugin.v1.WatchSyncDeviceAuthorizationServiceStartResponse + 21, // 79: silo.plugin.v1.WatchSyncDeviceAuthorizationService.Poll:output_type -> silo.plugin.v1.WatchSyncDeviceAuthorizationServicePollResponse + 71, // [71:80] is the sub-list for method output_type + 62, // [62:71] is the sub-list for method input_type + 62, // [62:62] is the sub-list for extension type_name + 62, // [62:62] is the sub-list for extension extendee + 0, // [0:62] is the sub-list for field type_name } func init() { file_silo_plugin_v1_watch_sync_provider_proto_init() } @@ -3132,7 +3278,7 @@ func file_silo_plugin_v1_watch_sync_provider_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_silo_plugin_v1_watch_sync_provider_proto_rawDesc), len(file_silo_plugin_v1_watch_sync_provider_proto_rawDesc)), NumEnums: 8, - NumMessages: 34, + NumMessages: 35, NumExtensions: 0, NumServices: 2, }, diff --git a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go index 2c1d4f1..5898990 100644 --- a/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go +++ b/pkg/pluginproto/silo/plugin/v1/watch_sync_provider_test.go @@ -159,6 +159,91 @@ func TestWatchSyncListTombstoneDoesNotRequireMedia(t *testing.T) { } } +func TestWatchSyncRatingRemoteStateRoundTrip(t *testing.T) { + ratedAt := time.Unix(1_750_000_000, 0).UTC() + input := &WatchSyncListRemoteStateResponse{ + Items: []*WatchSyncRemoteState{{ + ProviderItemKey: "show:7", + Media: &WatchSyncMedia{ + MediaItemId: "local-series-7", + MediaType: WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES, + Title: "Example Series", + Year: 2019, + ExternalIds: map[string]string{"tvdb": "12345"}, + }, + Rating: &WatchSyncRemoteRatingState{ + Rating: 8, + RatedAt: timestamppb.New(ratedAt), + }, + }}, + NextCursor: "ratings-1", + CompleteSnapshot: true, + } + + data, err := proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + var output WatchSyncListRemoteStateResponse + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + + item := output.GetItems()[0] + if !proto.Equal(input, &output) || + item.GetMedia().GetMediaType() != WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES || + item.GetMedia().GetExternalIds()["tvdb"] != "12345" || + item.GetRating().GetRating() != 8 || + !item.GetRating().GetRatedAt().AsTime().Equal(ratedAt) || + item.GetRating().GetRemoved() { + t.Fatalf("rating remote state = %#v", item) + } +} + +func TestWatchSyncRatingTombstoneDoesNotRequireMedia(t *testing.T) { + input := &WatchSyncRemoteState{ + ProviderItemKey: "movie:9", + Rating: &WatchSyncRemoteRatingState{Removed: true}, + } + data, err := proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + var output WatchSyncRemoteState + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if !proto.Equal(input, &output) || output.GetMedia() != nil || + !output.GetRating().GetRemoved() || output.GetRating().GetRating() != 0 { + t.Fatalf("rating tombstone = %#v", &output) + } +} + +func TestWatchSyncEventCarriesRating(t *testing.T) { + input := &WatchSyncEvent{ + EventId: "rating-1", + Operation: WatchSyncOperation_WATCH_SYNC_OPERATION_SET_RATING, + Media: &WatchSyncMedia{ + MediaItemId: "local-movie-9", + MediaType: WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE, + }, + Rating: 10, + } + data, err := proto.Marshal(input) + if err != nil { + t.Fatal(err) + } + var output WatchSyncEvent + if err := proto.Unmarshal(data, &output); err != nil { + t.Fatal(err) + } + if !proto.Equal(input, &output) || + output.GetOperation() != WatchSyncOperation_WATCH_SYNC_OPERATION_SET_RATING || + output.GetRating() != 10 { + t.Fatalf("rating event = %#v", &output) + } +} + func TestWatchSyncApplyResultCarriesTypedRateLimit(t *testing.T) { retryAfter := 45 * time.Second request := &WatchSyncApplyEventsRequest{ diff --git a/pkg/pluginsdk/convert/convert.go b/pkg/pluginsdk/convert/convert.go index 65fa2f0..1e4fd5c 100644 --- a/pkg/pluginsdk/convert/convert.go +++ b/pkg/pluginsdk/convert/convert.go @@ -10,6 +10,12 @@ import ( pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" ) +// capabilityUnmarshal decodes stored capability metadata tolerantly. Unknown +// fields and enum names are dropped rather than rejected, so a node on an older +// SDK, in a mixed-version cluster sharing one database, can still load +// descriptors written by a newer one. +var capabilityUnmarshal = protojson.UnmarshalOptions{DiscardUnknown: true} + type CapabilityRecord struct { Type string ID string @@ -70,7 +76,7 @@ func DecodeCapability(record CapabilityRecord) (*pluginv1.CapabilityDescriptor, return nil, fmt.Errorf("encode watch sync provider descriptor: %w", err) } var typed pluginv1.WatchSyncProviderDescriptor - if err := protojson.Unmarshal(data, &typed); err != nil { + if err := capabilityUnmarshal.Unmarshal(data, &typed); err != nil { return nil, fmt.Errorf("decode watch sync provider descriptor: %w", err) } descriptor.WatchSyncProvider = &typed @@ -177,7 +183,7 @@ func decodeConfigSchemas(value any) ([]*pluginv1.ConfigSchema, error) { return nil, fmt.Errorf("encode capability config schema: %w", err) } var schema pluginv1.ConfigSchema - if err := (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(jsonData, &schema); err != nil { + if err := capabilityUnmarshal.Unmarshal(jsonData, &schema); err != nil { return nil, fmt.Errorf("decode capability config schema: %w", err) } schemas = append(schemas, &schema) diff --git a/pkg/pluginsdk/convert/watch_sync_provider_test.go b/pkg/pluginsdk/convert/watch_sync_provider_test.go index 9c607b9..2e548d7 100644 --- a/pkg/pluginsdk/convert/watch_sync_provider_test.go +++ b/pkg/pluginsdk/convert/watch_sync_provider_test.go @@ -16,9 +16,12 @@ func TestWatchSyncProviderDescriptorRoundTrip(t *testing.T) { ExportWatched: true, ImportWatchlist: true, ScrobblePlayback: true, + ImportRatings: true, + ExportRatings: true, SupportedMediaTypes: []pluginv1.WatchSyncMediaType{ pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE, pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE, + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES, }, MaxBatchSize: 25, }, @@ -33,9 +36,51 @@ func TestWatchSyncProviderDescriptorRoundTrip(t *testing.T) { } got := decoded.GetWatchSyncProvider() if got == nil || !got.GetExportWatched() || !got.GetImportWatchlist() || !got.GetScrobblePlayback() || got.GetMaxBatchSize() != 25 || + !got.GetImportRatings() || !got.GetExportRatings() || len(got.GetAuthMethods()) != 1 || got.GetAuthMethods()[0] != pluginv1.WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_DEVICE_CODE || - len(got.GetSupportedMediaTypes()) != 2 || - got.GetSupportedMediaTypes()[1] != pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE { + len(got.GetSupportedMediaTypes()) != 3 || + got.GetSupportedMediaTypes()[1] != pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE || + got.GetSupportedMediaTypes()[2] != pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES { t.Fatalf("decoded descriptor = %#v", got) } } + +// A node on an older SDK must still load a descriptor that a newer SDK wrote +// to the shared database: unknown fields and enum names are dropped. +func TestDecodeCapabilityDiscardsUnknownWatchSyncData(t *testing.T) { + record := convert.CapabilityRecord{ + Type: "watch_sync_provider.v1", + ID: "anilist", + Metadata: map[string]any{ + "watch_sync_provider": map[string]any{ + "auth_methods": []any{"WATCH_SYNC_AUTH_METHOD_DEVICE_CODE"}, + "export_watched": true, + "max_batch_size": float64(25), + "supported_media_types": []any{"WATCH_SYNC_MEDIA_TYPE_MOVIE", "WATCH_SYNC_MEDIA_TYPE_FUTURE_KIND", "WATCH_SYNC_MEDIA_TYPE_EPISODE"}, + "import_future_thing": true, + }, + "config_schema": []any{map[string]any{"key": "token", "future_option": "x"}}, + }, + } + decoded, err := convert.DecodeCapability(record) + if err != nil { + t.Fatalf("DecodeCapability() error = %v", err) + } + got := decoded.GetWatchSyncProvider() + if got == nil || !got.GetExportWatched() || got.GetMaxBatchSize() != 25 || + len(got.GetAuthMethods()) != 1 || got.GetAuthMethods()[0] != pluginv1.WatchSyncAuthMethod_WATCH_SYNC_AUTH_METHOD_DEVICE_CODE { + t.Fatalf("decoded descriptor = %#v", got) + } + types := got.GetSupportedMediaTypes() + if len(types) != 2 || + types[0] != pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE || + types[1] != pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE { + t.Fatalf("supported media types = %v, want the known values only", types) + } + if len(got.ProtoReflect().GetUnknown()) != 0 { + t.Fatalf("unknown data retained: %x", got.ProtoReflect().GetUnknown()) + } + if schemas := decoded.GetConfigSchema(); len(schemas) != 1 || schemas[0].GetKey() != "token" { + t.Fatalf("config schema = %#v", schemas) + } +} diff --git a/pkg/pluginsdk/manifest/manifest.go b/pkg/pluginsdk/manifest/manifest.go index b826c83..0da82bd 100644 --- a/pkg/pluginsdk/manifest/manifest.go +++ b/pkg/pluginsdk/manifest/manifest.go @@ -164,7 +164,8 @@ func validateWatchSyncCapability(descriptor *pluginv1.CapabilityDescriptor) erro !watchSync.GetImportFavorites() && !watchSync.GetExportFavorites() && !watchSync.GetRemoveFavorites() && !watchSync.GetImportWatchlist() && !watchSync.GetExportWatchlist() && !watchSync.GetRemoveWatchlist() && - !watchSync.GetScrobblePlayback() { + !watchSync.GetScrobblePlayback() && + !watchSync.GetImportRatings() && !watchSync.GetExportRatings() { return fmt.Errorf("plugin capability %q: at least one watch sync operation is required", descriptor.GetId()) } if watchSync.GetProvidesWatchlistOrder() && !watchSync.GetImportWatchlist() { @@ -181,6 +182,11 @@ func validateWatchSyncCapability(descriptor *pluginv1.CapabilityDescriptor) erro return fmt.Errorf("plugin capability %q: watch sync media type cannot be unspecified", descriptor.GetId()) } } + // supported_media_types is non-empty here; an empty list is rejected above. + if (watchSync.GetImportRatings() || watchSync.GetExportRatings()) && + !watchSyncSupportsRatedMediaType(watchSync.GetSupportedMediaTypes()) { + return fmt.Errorf("plugin capability %q: watch sync ratings require a MOVIE or SERIES supported media type", descriptor.GetId()) + } for _, namespace := range watchSync.GetExternalIdNamespaces() { if !watchSyncSlugPattern.MatchString(namespace) { return fmt.Errorf("plugin capability %q: invalid external id namespace %q", descriptor.GetId(), namespace) @@ -189,6 +195,23 @@ func validateWatchSyncCapability(descriptor *pluginv1.CapabilityDescriptor) erro return nil } +// watchSyncSupportsRatedMediaType reports whether a ratings-capable provider +// lists a media type the host can rate. A value this SDK does not define counts +// as rateable so that a descriptor built against a newer SDK stays valid here. +func watchSyncSupportsRatedMediaType(mediaTypes []pluginv1.WatchSyncMediaType) bool { + for _, mediaType := range mediaTypes { + switch mediaType { + case pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE, + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES: + return true + } + if _, known := pluginv1.WatchSyncMediaType_name[int32(mediaType)]; !known { + return true + } + } + return false +} + func validateNetworkAccessCapability(descriptor *pluginv1.CapabilityDescriptor) error { networkAccess := descriptor.GetNetworkAccessProvider() if descriptor.GetType() != capability.NetworkAccessProvider { diff --git a/pkg/pluginsdk/manifest/watch_sync_provider_test.go b/pkg/pluginsdk/manifest/watch_sync_provider_test.go index 115b992..25e30d1 100644 --- a/pkg/pluginsdk/manifest/watch_sync_provider_test.go +++ b/pkg/pluginsdk/manifest/watch_sync_provider_test.go @@ -109,6 +109,121 @@ func TestValidateWatchSyncProviderRejectsOrderWithoutImport(t *testing.T) { } } +func TestLoadWatchSyncProviderRatingsOnly(t *testing.T) { + raw := []byte(`{ + "plugin_id":"silo.ratings", "version":"1.0.0", "silo_api_version":"v1", + "capabilities":[{ + "type":"watch_sync_provider.v1", "id":"ratings", "display_name":"Ratings", + "watch_sync_provider":{ + "auth_methods":["WATCH_SYNC_AUTH_METHOD_API_KEY"], + "import_ratings":true, + "export_ratings":true, + "supported_media_types":["WATCH_SYNC_MEDIA_TYPE_MOVIE","WATCH_SYNC_MEDIA_TYPE_SERIES"], + "max_batch_size":10 + } + }] + }`) + manifest, err := publicmanifest.Load(raw) + if err != nil { + t.Fatalf("ratings-only provider should load: %v", err) + } + descriptor := manifest.GetCapabilities()[0].GetWatchSyncProvider() + if !descriptor.GetImportRatings() || !descriptor.GetExportRatings() || + len(descriptor.GetSupportedMediaTypes()) != 2 || + descriptor.GetSupportedMediaTypes()[1] != pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES { + t.Fatalf("watch sync descriptor = %#v", descriptor) + } +} + +func TestValidateWatchSyncProviderAllowsRatingsOnly(t *testing.T) { + for _, tc := range []struct { + name string + importRatings bool + exportRatings bool + }{ + {name: "import", importRatings: true}, + {name: "export", exportRatings: true}, + } { + t.Run(tc.name, func(t *testing.T) { + manifest := validWatchSyncManifest() + descriptor := manifest.Capabilities[0].WatchSyncProvider + descriptor.ExportWatched = false + descriptor.ImportRatings = tc.importRatings + descriptor.ExportRatings = tc.exportRatings + if err := publicmanifest.Validate(manifest); err != nil { + t.Fatalf("ratings-only provider should be valid: %v", err) + } + }) + } +} + +func TestValidateWatchSyncProviderRejectsRatingsWithoutRateableMediaType(t *testing.T) { + for _, tc := range []struct { + name string + importRatings bool + exportRatings bool + }{ + {name: "import", importRatings: true}, + {name: "export", exportRatings: true}, + } { + t.Run(tc.name, func(t *testing.T) { + manifest := validWatchSyncManifest() + descriptor := manifest.Capabilities[0].WatchSyncProvider + descriptor.ImportRatings = tc.importRatings + descriptor.ExportRatings = tc.exportRatings + descriptor.SupportedMediaTypes = []pluginv1.WatchSyncMediaType{ + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE, + } + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected ratings with only EPISODE media to fail") + } + }) + } +} + +func TestValidateWatchSyncProviderAllowsEpisodeOnlyWithoutRatings(t *testing.T) { + manifest := validWatchSyncManifest() + manifest.Capabilities[0].WatchSyncProvider.SupportedMediaTypes = []pluginv1.WatchSyncMediaType{ + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE, + } + if err := publicmanifest.Validate(manifest); err != nil { + t.Fatalf("episode-only provider without ratings should be valid: %v", err) + } +} + +func TestValidateWatchSyncProviderAllowsRatingsForRateableMediaTypes(t *testing.T) { + for _, tc := range []struct { + name string + mediaTypes []pluginv1.WatchSyncMediaType + }{ + {name: "series", mediaTypes: []pluginv1.WatchSyncMediaType{ + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES, + }}, + {name: "episode and series", mediaTypes: []pluginv1.WatchSyncMediaType{ + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE, + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_SERIES, + }}, + {name: "episode and movie", mediaTypes: []pluginv1.WatchSyncMediaType{ + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_EPISODE, + pluginv1.WatchSyncMediaType_WATCH_SYNC_MEDIA_TYPE_MOVIE, + }}, + {name: "unknown future type", mediaTypes: []pluginv1.WatchSyncMediaType{ + pluginv1.WatchSyncMediaType(99), + }}, + } { + t.Run(tc.name, func(t *testing.T) { + manifest := validWatchSyncManifest() + descriptor := manifest.Capabilities[0].WatchSyncProvider + descriptor.ImportRatings = true + descriptor.ExportRatings = true + descriptor.SupportedMediaTypes = tc.mediaTypes + if err := publicmanifest.Validate(manifest); err != nil { + t.Fatalf("ratings provider should be valid: %v", err) + } + }) + } +} + func validWatchSyncManifest() *pluginv1.PluginManifest { return &pluginv1.PluginManifest{ PluginId: "silo.valid", Version: "1.0.0", diff --git a/proto/silo/plugin/v1/watch_sync_provider.proto b/proto/silo/plugin/v1/watch_sync_provider.proto index b8b0033..4de307c 100644 --- a/proto/silo/plugin/v1/watch_sync_provider.proto +++ b/proto/silo/plugin/v1/watch_sync_provider.proto @@ -41,6 +41,10 @@ enum WatchSyncMediaType { WATCH_SYNC_MEDIA_TYPE_UNSPECIFIED = 0; WATCH_SYNC_MEDIA_TYPE_MOVIE = 1; WATCH_SYNC_MEDIA_TYPE_EPISODE = 2; + // A series-level item. In WatchSyncMedia, external_ids, title, and year + // describe the series itself; the series_* fields, season_number, + // episode_number, and absolute_episode_number are unused. + WATCH_SYNC_MEDIA_TYPE_SERIES = 3; } enum WatchSyncOperation { @@ -54,6 +58,10 @@ enum WatchSyncOperation { WATCH_SYNC_OPERATION_SCROBBLE_START = 7; WATCH_SYNC_OPERATION_SCROBBLE_PAUSE = 8; WATCH_SYNC_OPERATION_SCROBBLE_STOP = 9; + // Rating writes carry the desired value in WatchSyncEvent.rating. Both are + // convergent desired-state operations; see WatchSyncEvent.rating. + WATCH_SYNC_OPERATION_SET_RATING = 10; + WATCH_SYNC_OPERATION_REMOVE_RATING = 11; } enum WatchSyncRemoteStateKind { @@ -62,6 +70,7 @@ enum WatchSyncRemoteStateKind { WATCH_SYNC_REMOTE_STATE_KIND_PROGRESS = 2; WATCH_SYNC_REMOTE_STATE_KIND_FAVORITE = 3; WATCH_SYNC_REMOTE_STATE_KIND_WATCHLIST = 4; + WATCH_SYNC_REMOTE_STATE_KIND_RATING = 5; } enum WatchSyncDeviceAuthorizationStatus { @@ -117,6 +126,17 @@ message WatchSyncProviderDescriptor { // an incremental subset cannot define positions relative to omitted items. bool provides_watchlist_order = 15; bool scrobble_playback = 16; + // Ratings are integers from 1 to 10. Plugins convert between the provider's + // native scale and 1..10 by rounding half up and clamping to the valid range. + // The host sends rating events only for media types listed in + // supported_media_types, which must include MOVIE or SERIES when either + // ratings flag is set. + // + // When import_ratings is set, ListRemoteState returns RATING states. + bool import_ratings = 17; + // When export_ratings is set, ApplyEvents handles both SET_RATING and + // REMOVE_RATING; there is no separate flag for rating removal. + bool export_ratings = 18; } message WatchSyncProviderConfig { @@ -313,6 +333,12 @@ message WatchSyncEvent { // exist for incomplete playback, and completion_percent may be below 100 // when the host's configured watched threshold has been reached. bool completed = 14; + // Desired rating for SET_RATING, an integer from 1 to 10. Required for + // SET_RATING and ignored for every other operation; zero is never a valid + // rating. SET_RATING and REMOVE_RATING are convergent desired-state writes: + // resending the same value, or removing a rating that is already absent, + // must yield APPLIED or NO_CHANGE, never a fault. + int32 rating = 15; } message WatchSyncApplyEventsRequest { @@ -382,10 +408,22 @@ message WatchSyncRemoteListState { bool removed = 2; } +message WatchSyncRemoteRatingState { + // Integer rating from 1 to 10, converted from the provider's native scale. + // Required unless removed is true. + int32 rating = 1; + // Provider timestamp for when the rating was last set, when available. + google.protobuf.Timestamp rated_at = 2; + // True is an explicit tombstone for a rating cleared upstream. It is + // meaningful for incremental traversals, where omitting an item is not a + // deletion. + bool removed = 3; +} + message WatchSyncRemoteState { string provider_item_key = 1; - // Required except on an explicit favorite/watchlist tombstone whose - // provider_item_key identifies a record returned previously. + // Required except on an explicit favorite, watchlist, or rating tombstone + // whose provider_item_key identifies a record returned previously. WatchSyncMedia media = 2; // At least one typed state must be present. Both may be present when the // provider reports a completed play and a separate resume point. @@ -393,6 +431,7 @@ message WatchSyncRemoteState { WatchSyncRemoteProgressState progress = 4; WatchSyncRemoteListState favorite = 5; WatchSyncRemoteListState watchlist = 6; + WatchSyncRemoteRatingState rating = 7; } message WatchSyncListRemoteStateResponse { @@ -407,7 +446,8 @@ message WatchSyncListRemoteStateResponse { // incremental delta, so an absent item must not be interpreted as deletion. // The value must remain stable across every page in one traversal. A provider // advertising provides_watchlist_order must set this true for WATCHLIST - // traversals so the returned order is unambiguous. + // traversals so the returned order is unambiguous. In a complete RATING + // traversal, an item absent from the snapshot is unrated. bool complete_snapshot = 4; // Complete authoritative replacement. The host persists it before consuming // this page or fault; a persistence failure discards the page and cursor. From 6c1da001ea147a44b1c9599ed58a7aa981410e27 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Sun, 27 Sep 2026 01:33:53 -0400 Subject: [PATCH 16/17] feat(requestrouter): carry requested seasons to request routers (#25) Series requests can name seasons, but RequestDescriptor had no field for them, so a request router could only add the whole series. The host therefore refuses to request the missing seasons of a series it already has whenever a download server takes series. Add, both appended without renumbering: - RequestDescriptor.seasons = 10. Empty means the whole series; 0 is Specials. - CapabilityDescriptor.request_router = 12, a RequestRouterDescriptor with supports_seasons = 1. Plugins that honour seasons set it; an absent descriptor, as in every plugin built on an older SDK, means false, so the host can tell the two apart from the manifest. The manifest validator keeps the descriptor optional and rejects it on other capability types. convert round-trips it so the host can read the flag from stored capability metadata. Closes #24 Co-authored-by: Claude Opus 5.5 (1M context) --- README.md | 31 ++ docs/compatibility.md | 8 + pkg/pluginproto/silo/plugin/v1/common.pb.go | 67 +++-- .../silo/plugin/v1/request_router.pb.go | 283 +++++++++++------- pkg/pluginsdk/convert/convert.go | 22 ++ pkg/pluginsdk/convert/request_router_test.go | 61 ++++ pkg/pluginsdk/manifest/manifest.go | 12 + pkg/pluginsdk/manifest/request_router_test.go | 55 ++++ pkg/pluginsdk/runtime/request_router_test.go | 57 ++++ proto/silo/plugin/v1/common.proto | 6 + proto/silo/plugin/v1/request_router.proto | 21 ++ 11 files changed, 492 insertions(+), 131 deletions(-) create mode 100644 pkg/pluginsdk/convert/request_router_test.go create mode 100644 pkg/pluginsdk/manifest/request_router_test.go create mode 100644 pkg/pluginsdk/runtime/request_router_test.go diff --git a/README.md b/README.md index ebad6e4..ef8dd3c 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,37 @@ err = host.CallPluginJSON(ctx, runtimehost.CallPluginJSONRequest{ The `auth_provider.v1` capability also exposes OAuth-flow RPCs (`InitAuthorize`, `ExchangeCode`, `RefreshSession`) for plugins that wrap external identity providers. +## Request routers + +`request_router.v1` lets the host hand a media request to a download backend +such as Sonarr, Radarr, or Seerr. The host owns the request lifecycle, policy, +and quality governance; the plugin routes the request to a configured +connection and reports its status. + +A series request may name seasons in `RequestDescriptor.seasons`. Season `0` +is Specials, and an empty list means the whole series. A plugin that fulfils +seasons individually declares it in its manifest: + +```json +{ + "type": "request_router.v1", + "id": "arr", + "request_router": { "supports_seasons": true } +} +``` + +A declaring plugin must acquire only the requested seasons. When the series +already exists upstream, it adds those seasons to what is already tracked and +leaves the other seasons alone, so a request for season 4 never stops tracking +seasons 1–3. Repeating a request must converge rather than add the series +again. `CheckStatus` receives the same descriptor, so status can cover the +requested seasons. + +Plugins without the flag, including every plugin built before it existed, keep +today's whole-series behaviour. The host requests only the missing seasons of a +series it already has from plugins that declare `supports_seasons`, because +any other plugin would add the whole series again. + ## Watch sync providers `watch_sync_provider.v1` lets external plugins participate in Silo's host-owned diff --git a/docs/compatibility.md b/docs/compatibility.md index 953996c..9e28692 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -68,6 +68,14 @@ Watch-sync rating fields (`WatchSyncEvent.rating` and ratings run from 1 to 10, so zero never carries a rating and no presence check is needed. +`RequestDescriptor.seasons` is a repeated field, so it has no presence: an +empty list means the whole series, and season `0` in a non-empty list means +Specials. A plugin built before the field existed decodes it as an unknown +field and fulfils the whole series. Because the plugin cannot say so on the +wire, the host decides who may receive a season-only request from the +manifest's `RequestRouterDescriptor.supports_seasons` flag instead. An absent +descriptor means the flag is false. + A season-scoped `GetImagesRequest` is a scope, not a guarantee. Plugins that can filter by season should do so, and plugins should populate `ImageRecord.season_number` whenever the season is known. Hosts must bucket and diff --git a/pkg/pluginproto/silo/plugin/v1/common.pb.go b/pkg/pluginproto/silo/plugin/v1/common.pb.go index 267874d..ba74bcd 100644 --- a/pkg/pluginproto/silo/plugin/v1/common.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/common.pb.go @@ -981,8 +981,12 @@ type CapabilityDescriptor struct { // of type "network_access_provider.v1". The host lists available overlay // providers from this descriptor without launching the plugin. NetworkAccessProvider *NetworkAccessProviderDescriptor `protobuf:"bytes,11,opt,name=network_access_provider,json=networkAccessProvider,proto3" json:"network_access_provider,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Typed request-router contract metadata. Only meaningful for capabilities + // of type "request_router.v1". Optional: an absent descriptor declares no + // optional request-router features. + RequestRouter *RequestRouterDescriptor `protobuf:"bytes,12,opt,name=request_router,json=requestRouter,proto3" json:"request_router,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CapabilityDescriptor) Reset() { @@ -1092,6 +1096,13 @@ func (x *CapabilityDescriptor) GetNetworkAccessProvider() *NetworkAccessProvider return nil } +func (x *CapabilityDescriptor) GetRequestRouter() *RequestRouterDescriptor { + if x != nil { + return x.RequestRouter + } + return nil +} + // PluginPresentation describes a plugin for server operators and links them to // its publisher, source, support, and release history. These values are // self-declared by the plugin and never determine catalog approval. @@ -1610,7 +1621,7 @@ var File_silo_plugin_v1_common_proto protoreflect.FileDescriptor const file_silo_plugin_v1_common_proto_rawDesc = "" + "\n" + - "\x1bsilo/plugin/v1/common.proto\x12\x0esilo.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a,silo/plugin/v1/network_access_provider.proto\x1a(silo/plugin/v1/watch_sync_provider.proto\"7\n" + + "\x1bsilo/plugin/v1/common.proto\x12\x0esilo.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a,silo/plugin/v1/network_access_provider.proto\x1a#silo/plugin/v1/request_router.proto\x1a(silo/plugin/v1/watch_sync_provider.proto\"7\n" + "\x11SupportedPlatform\x12\x0e\n" + "\x02os\x18\x01 \x01(\tR\x02os\x12\x12\n" + "\x04arch\x18\x02 \x01(\tR\x04arch\"\xd9\x01\n" + @@ -1687,7 +1698,7 @@ const file_silo_plugin_v1_common_proto_rawDesc = "" + "\rPackagedAsset\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12!\n" + "\fcontent_type\x18\x02 \x01(\tR\vcontentType\x12\x1c\n" + - "\tintegrity\x18\x03 \x01(\tR\tintegrity\"\x9d\x04\n" + + "\tintegrity\x18\x03 \x01(\tR\tintegrity\"\xed\x04\n" + "\x14CapabilityDescriptor\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x0e\n" + "\x02id\x18\x02 \x01(\tR\x02id\x12!\n" + @@ -1701,7 +1712,8 @@ const file_silo_plugin_v1_common_proto_rawDesc = "" + "\bicon_url\x18\t \x01(\tR\aiconUrl\x12[\n" + "\x13watch_sync_provider\x18\n" + " \x01(\v2+.silo.plugin.v1.WatchSyncProviderDescriptorR\x11watchSyncProvider\x12g\n" + - "\x17network_access_provider\x18\v \x01(\v2/.silo.plugin.v1.NetworkAccessProviderDescriptorR\x15networkAccessProvider\"\xa2\x03\n" + + "\x17network_access_provider\x18\v \x01(\v2/.silo.plugin.v1.NetworkAccessProviderDescriptorR\x15networkAccessProvider\x12N\n" + + "\x0erequest_router\x18\f \x01(\v2'.silo.plugin.v1.RequestRouterDescriptorR\rrequestRouter\"\xa2\x03\n" + "\x12PluginPresentation\x12!\n" + "\fdisplay_name\x18\x01 \x01(\tR\vdisplayName\x12\x18\n" + "\asummary\x18\x02 \x01(\tR\asummary\x121\n" + @@ -1796,6 +1808,7 @@ var file_silo_plugin_v1_common_proto_goTypes = []any{ (*structpb.Struct)(nil), // 22: google.protobuf.Struct (*WatchSyncProviderDescriptor)(nil), // 23: silo.plugin.v1.WatchSyncProviderDescriptor (*NetworkAccessProviderDescriptor)(nil), // 24: silo.plugin.v1.NetworkAccessProviderDescriptor + (*RequestRouterDescriptor)(nil), // 25: silo.plugin.v1.RequestRouterDescriptor } var file_silo_plugin_v1_common_proto_depIdxs = []int32{ 8, // 0: silo.plugin.v1.ConfigSchema.admin_form:type_name -> silo.plugin.v1.AdminFormDescriptor @@ -1812,27 +1825,28 @@ var file_silo_plugin_v1_common_proto_depIdxs = []int32{ 22, // 11: silo.plugin.v1.CapabilityDescriptor.metadata:type_name -> google.protobuf.Struct 23, // 12: silo.plugin.v1.CapabilityDescriptor.watch_sync_provider:type_name -> silo.plugin.v1.WatchSyncProviderDescriptor 24, // 13: silo.plugin.v1.CapabilityDescriptor.network_access_provider:type_name -> silo.plugin.v1.NetworkAccessProviderDescriptor - 1, // 14: silo.plugin.v1.PluginManifest.supported_platforms:type_name -> silo.plugin.v1.SupportedPlatform - 12, // 15: silo.plugin.v1.PluginManifest.capabilities:type_name -> silo.plugin.v1.CapabilityDescriptor - 2, // 16: silo.plugin.v1.PluginManifest.global_config_schema:type_name -> silo.plugin.v1.ConfigSchema - 2, // 17: silo.plugin.v1.PluginManifest.user_config_schema:type_name -> silo.plugin.v1.ConfigSchema - 10, // 18: silo.plugin.v1.PluginManifest.http_routes:type_name -> silo.plugin.v1.HttpRouteDescriptor - 11, // 19: silo.plugin.v1.PluginManifest.assets:type_name -> silo.plugin.v1.PackagedAsset - 22, // 20: silo.plugin.v1.PluginManifest.metadata:type_name -> google.protobuf.Struct - 13, // 21: silo.plugin.v1.PluginManifest.presentation:type_name -> silo.plugin.v1.PluginPresentation - 14, // 22: silo.plugin.v1.GetManifestResponse.manifest:type_name -> silo.plugin.v1.PluginManifest - 9, // 23: silo.plugin.v1.ConfigureRequest.config:type_name -> silo.plugin.v1.ConfigEntry - 15, // 24: silo.plugin.v1.Runtime.GetManifest:input_type -> silo.plugin.v1.GetManifestRequest - 17, // 25: silo.plugin.v1.Runtime.Configure:input_type -> silo.plugin.v1.ConfigureRequest - 19, // 26: silo.plugin.v1.Runtime.BindHostBroker:input_type -> silo.plugin.v1.BindHostBrokerRequest - 16, // 27: silo.plugin.v1.Runtime.GetManifest:output_type -> silo.plugin.v1.GetManifestResponse - 18, // 28: silo.plugin.v1.Runtime.Configure:output_type -> silo.plugin.v1.ConfigureResponse - 20, // 29: silo.plugin.v1.Runtime.BindHostBroker:output_type -> silo.plugin.v1.BindHostBrokerResponse - 27, // [27:30] is the sub-list for method output_type - 24, // [24:27] is the sub-list for method input_type - 24, // [24:24] is the sub-list for extension type_name - 24, // [24:24] is the sub-list for extension extendee - 0, // [0:24] is the sub-list for field type_name + 25, // 14: silo.plugin.v1.CapabilityDescriptor.request_router:type_name -> silo.plugin.v1.RequestRouterDescriptor + 1, // 15: silo.plugin.v1.PluginManifest.supported_platforms:type_name -> silo.plugin.v1.SupportedPlatform + 12, // 16: silo.plugin.v1.PluginManifest.capabilities:type_name -> silo.plugin.v1.CapabilityDescriptor + 2, // 17: silo.plugin.v1.PluginManifest.global_config_schema:type_name -> silo.plugin.v1.ConfigSchema + 2, // 18: silo.plugin.v1.PluginManifest.user_config_schema:type_name -> silo.plugin.v1.ConfigSchema + 10, // 19: silo.plugin.v1.PluginManifest.http_routes:type_name -> silo.plugin.v1.HttpRouteDescriptor + 11, // 20: silo.plugin.v1.PluginManifest.assets:type_name -> silo.plugin.v1.PackagedAsset + 22, // 21: silo.plugin.v1.PluginManifest.metadata:type_name -> google.protobuf.Struct + 13, // 22: silo.plugin.v1.PluginManifest.presentation:type_name -> silo.plugin.v1.PluginPresentation + 14, // 23: silo.plugin.v1.GetManifestResponse.manifest:type_name -> silo.plugin.v1.PluginManifest + 9, // 24: silo.plugin.v1.ConfigureRequest.config:type_name -> silo.plugin.v1.ConfigEntry + 15, // 25: silo.plugin.v1.Runtime.GetManifest:input_type -> silo.plugin.v1.GetManifestRequest + 17, // 26: silo.plugin.v1.Runtime.Configure:input_type -> silo.plugin.v1.ConfigureRequest + 19, // 27: silo.plugin.v1.Runtime.BindHostBroker:input_type -> silo.plugin.v1.BindHostBrokerRequest + 16, // 28: silo.plugin.v1.Runtime.GetManifest:output_type -> silo.plugin.v1.GetManifestResponse + 18, // 29: silo.plugin.v1.Runtime.Configure:output_type -> silo.plugin.v1.ConfigureResponse + 20, // 30: silo.plugin.v1.Runtime.BindHostBroker:output_type -> silo.plugin.v1.BindHostBrokerResponse + 28, // [28:31] is the sub-list for method output_type + 25, // [25:28] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_silo_plugin_v1_common_proto_init() } @@ -1841,6 +1855,7 @@ func file_silo_plugin_v1_common_proto_init() { return } file_silo_plugin_v1_network_access_provider_proto_init() + file_silo_plugin_v1_request_router_proto_init() file_silo_plugin_v1_watch_sync_provider_proto_init() type x struct{} out := protoimpl.TypeBuilder{ diff --git a/pkg/pluginproto/silo/plugin/v1/request_router.pb.go b/pkg/pluginproto/silo/plugin/v1/request_router.pb.go index e30d83a..6ef7377 100644 --- a/pkg/pluginproto/silo/plugin/v1/request_router.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/request_router.pb.go @@ -35,8 +35,14 @@ type RequestDescriptor struct { // on the downstream service (e.g. Seerr). Empty when the host cannot resolve it. RequesterEmail string `protobuf:"bytes,8,opt,name=requester_email,json=requesterEmail,proto3" json:"requester_email,omitempty"` RequesterUsername string `protobuf:"bytes,9,opt,name=requester_username,json=requesterUsername,proto3" json:"requester_username,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // seasons lists the requested season numbers of a series; 0 is Specials. + // Empty means the whole series, which is what every request meant before + // this field existed. Only set when media_type is "series". Plugins that + // declare RequestRouterDescriptor.supports_seasons must fulfil only these + // seasons; see that field for the full contract. + Seasons []int32 `protobuf:"varint,10,rep,packed,name=seasons,proto3" json:"seasons,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RequestDescriptor) Reset() { @@ -132,6 +138,68 @@ func (x *RequestDescriptor) GetRequesterUsername() string { return "" } +func (x *RequestDescriptor) GetSeasons() []int32 { + if x != nil { + return x.Seasons + } + return nil +} + +// RequestRouterDescriptor is typed request_router.v1 contract metadata that the +// host reads from the manifest without launching the plugin. An absent +// descriptor means every flag is false, which is how the host treats plugins +// built before a flag existed. +type RequestRouterDescriptor struct { + state protoimpl.MessageState `protogen:"open.v1"` + // supports_seasons declares that Fulfill and CheckStatus honour + // RequestDescriptor.seasons. A declaring plugin acquires only the listed + // seasons; when the series already exists upstream it adds those seasons to + // what is already tracked and leaves other seasons unchanged. Repeating a + // request converges rather than duplicating work. The host sends a request + // for only the missing seasons of a series it already has only to plugins + // that set this flag, because other plugins would add the whole series. + SupportsSeasons bool `protobuf:"varint,1,opt,name=supports_seasons,json=supportsSeasons,proto3" json:"supports_seasons,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RequestRouterDescriptor) Reset() { + *x = RequestRouterDescriptor{} + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RequestRouterDescriptor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestRouterDescriptor) ProtoMessage() {} + +func (x *RequestRouterDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RequestRouterDescriptor.ProtoReflect.Descriptor instead. +func (*RequestRouterDescriptor) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{1} +} + +func (x *RequestRouterDescriptor) GetSupportsSeasons() bool { + if x != nil { + return x.SupportsSeasons + } + return false +} + type RouterConnection struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -146,7 +214,7 @@ type RouterConnection struct { func (x *RouterConnection) Reset() { *x = RouterConnection{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[1] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -158,7 +226,7 @@ func (x *RouterConnection) String() string { func (*RouterConnection) ProtoMessage() {} func (x *RouterConnection) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[1] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -171,7 +239,7 @@ func (x *RouterConnection) ProtoReflect() protoreflect.Message { // Deprecated: Use RouterConnection.ProtoReflect.Descriptor instead. func (*RouterConnection) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{1} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{2} } func (x *RouterConnection) GetId() string { @@ -216,7 +284,7 @@ type RequestedQuality struct { func (x *RequestedQuality) Reset() { *x = RequestedQuality{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[2] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -228,7 +296,7 @@ func (x *RequestedQuality) String() string { func (*RequestedQuality) ProtoMessage() {} func (x *RequestedQuality) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[2] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -241,7 +309,7 @@ func (x *RequestedQuality) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestedQuality.ProtoReflect.Descriptor instead. func (*RequestedQuality) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{2} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{3} } func (x *RequestedQuality) GetId() string { @@ -270,7 +338,7 @@ type FulfillRequest struct { func (x *FulfillRequest) Reset() { *x = FulfillRequest{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[3] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -282,7 +350,7 @@ func (x *FulfillRequest) String() string { func (*FulfillRequest) ProtoMessage() {} func (x *FulfillRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[3] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -295,7 +363,7 @@ func (x *FulfillRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FulfillRequest.ProtoReflect.Descriptor instead. func (*FulfillRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{3} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{4} } func (x *FulfillRequest) GetCapabilityId() string { @@ -343,7 +411,7 @@ type FulfillmentTarget struct { func (x *FulfillmentTarget) Reset() { *x = FulfillmentTarget{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[4] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -355,7 +423,7 @@ func (x *FulfillmentTarget) String() string { func (*FulfillmentTarget) ProtoMessage() {} func (x *FulfillmentTarget) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[4] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -368,7 +436,7 @@ func (x *FulfillmentTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use FulfillmentTarget.ProtoReflect.Descriptor instead. func (*FulfillmentTarget) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{4} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{5} } func (x *FulfillmentTarget) GetQuality() string { @@ -424,7 +492,7 @@ type FulfillResponse struct { func (x *FulfillResponse) Reset() { *x = FulfillResponse{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[5] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -436,7 +504,7 @@ func (x *FulfillResponse) String() string { func (*FulfillResponse) ProtoMessage() {} func (x *FulfillResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[5] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -449,7 +517,7 @@ func (x *FulfillResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FulfillResponse.ProtoReflect.Descriptor instead. func (*FulfillResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{5} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{6} } func (x *FulfillResponse) GetTargets() []*FulfillmentTarget { @@ -477,7 +545,7 @@ type TargetRef struct { func (x *TargetRef) Reset() { *x = TargetRef{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[6] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -489,7 +557,7 @@ func (x *TargetRef) String() string { func (*TargetRef) ProtoMessage() {} func (x *TargetRef) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[6] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -502,7 +570,7 @@ func (x *TargetRef) ProtoReflect() protoreflect.Message { // Deprecated: Use TargetRef.ProtoReflect.Descriptor instead. func (*TargetRef) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{6} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{7} } func (x *TargetRef) GetQuality() string { @@ -538,7 +606,7 @@ type CheckStatusRequest struct { func (x *CheckStatusRequest) Reset() { *x = CheckStatusRequest{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[7] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -550,7 +618,7 @@ func (x *CheckStatusRequest) String() string { func (*CheckStatusRequest) ProtoMessage() {} func (x *CheckStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[7] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -563,7 +631,7 @@ func (x *CheckStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckStatusRequest.ProtoReflect.Descriptor instead. func (*CheckStatusRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{7} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{8} } func (x *CheckStatusRequest) GetCapabilityId() string { @@ -608,7 +676,7 @@ type TargetStatus struct { func (x *TargetStatus) Reset() { *x = TargetStatus{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[8] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -620,7 +688,7 @@ func (x *TargetStatus) String() string { func (*TargetStatus) ProtoMessage() {} func (x *TargetStatus) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[8] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -633,7 +701,7 @@ func (x *TargetStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use TargetStatus.ProtoReflect.Descriptor instead. func (*TargetStatus) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{8} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{9} } func (x *TargetStatus) GetQuality() string { @@ -680,7 +748,7 @@ type CheckStatusResponse struct { func (x *CheckStatusResponse) Reset() { *x = CheckStatusResponse{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[9] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -692,7 +760,7 @@ func (x *CheckStatusResponse) String() string { func (*CheckStatusResponse) ProtoMessage() {} func (x *CheckStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[9] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -705,7 +773,7 @@ func (x *CheckStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckStatusResponse.ProtoReflect.Descriptor instead. func (*CheckStatusResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{9} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{10} } func (x *CheckStatusResponse) GetStatuses() []*TargetStatus { @@ -725,7 +793,7 @@ type ConfigOption struct { func (x *ConfigOption) Reset() { *x = ConfigOption{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[10] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -737,7 +805,7 @@ func (x *ConfigOption) String() string { func (*ConfigOption) ProtoMessage() {} func (x *ConfigOption) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[10] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -750,7 +818,7 @@ func (x *ConfigOption) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigOption.ProtoReflect.Descriptor instead. func (*ConfigOption) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{10} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{11} } func (x *ConfigOption) GetValue() string { @@ -776,7 +844,7 @@ type ConfigOptionList struct { func (x *ConfigOptionList) Reset() { *x = ConfigOptionList{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[11] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -788,7 +856,7 @@ func (x *ConfigOptionList) String() string { func (*ConfigOptionList) ProtoMessage() {} func (x *ConfigOptionList) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[11] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -801,7 +869,7 @@ func (x *ConfigOptionList) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigOptionList.ProtoReflect.Descriptor instead. func (*ConfigOptionList) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{11} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{12} } func (x *ConfigOptionList) GetOptions() []*ConfigOption { @@ -821,7 +889,7 @@ type ListConfigOptionsRequest struct { func (x *ListConfigOptionsRequest) Reset() { *x = ListConfigOptionsRequest{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[12] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -833,7 +901,7 @@ func (x *ListConfigOptionsRequest) String() string { func (*ListConfigOptionsRequest) ProtoMessage() {} func (x *ListConfigOptionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[12] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -846,7 +914,7 @@ func (x *ListConfigOptionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListConfigOptionsRequest.ProtoReflect.Descriptor instead. func (*ListConfigOptionsRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{12} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{13} } func (x *ListConfigOptionsRequest) GetCapabilityId() string { @@ -873,7 +941,7 @@ type ListConfigOptionsResponse struct { func (x *ListConfigOptionsResponse) Reset() { *x = ListConfigOptionsResponse{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[13] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -885,7 +953,7 @@ func (x *ListConfigOptionsResponse) String() string { func (*ListConfigOptionsResponse) ProtoMessage() {} func (x *ListConfigOptionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[13] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -898,7 +966,7 @@ func (x *ListConfigOptionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListConfigOptionsResponse.ProtoReflect.Descriptor instead. func (*ListConfigOptionsResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{13} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{14} } func (x *ListConfigOptionsResponse) GetOptionsByField() map[string]*ConfigOptionList { @@ -918,7 +986,7 @@ type TestConnectionRequest struct { func (x *TestConnectionRequest) Reset() { *x = TestConnectionRequest{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[14] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -930,7 +998,7 @@ func (x *TestConnectionRequest) String() string { func (*TestConnectionRequest) ProtoMessage() {} func (x *TestConnectionRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[14] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -943,7 +1011,7 @@ func (x *TestConnectionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TestConnectionRequest.ProtoReflect.Descriptor instead. func (*TestConnectionRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{14} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{15} } func (x *TestConnectionRequest) GetCapabilityId() string { @@ -970,7 +1038,7 @@ type TestConnectionResponse struct { func (x *TestConnectionResponse) Reset() { *x = TestConnectionResponse{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[15] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -982,7 +1050,7 @@ func (x *TestConnectionResponse) String() string { func (*TestConnectionResponse) ProtoMessage() {} func (x *TestConnectionResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[15] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -995,7 +1063,7 @@ func (x *TestConnectionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TestConnectionResponse.ProtoReflect.Descriptor instead. func (*TestConnectionResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{15} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{16} } func (x *TestConnectionResponse) GetOk() bool { @@ -1027,7 +1095,7 @@ type ValidateRequest struct { func (x *ValidateRequest) Reset() { *x = ValidateRequest{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[16] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1039,7 +1107,7 @@ func (x *ValidateRequest) String() string { func (*ValidateRequest) ProtoMessage() {} func (x *ValidateRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[16] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1052,7 +1120,7 @@ func (x *ValidateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateRequest.ProtoReflect.Descriptor instead. func (*ValidateRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{16} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{17} } func (x *ValidateRequest) GetCapabilityId() string { @@ -1086,7 +1154,7 @@ type ValidateResponse struct { func (x *ValidateResponse) Reset() { *x = ValidateResponse{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[17] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1098,7 +1166,7 @@ func (x *ValidateResponse) String() string { func (*ValidateResponse) ProtoMessage() {} func (x *ValidateResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[17] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1111,7 +1179,7 @@ func (x *ValidateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateResponse.ProtoReflect.Descriptor instead. func (*ValidateResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{17} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{18} } func (x *ValidateResponse) GetFieldErrors() map[string]string { @@ -1132,7 +1200,7 @@ var File_silo_plugin_v1_request_router_proto protoreflect.FileDescriptor const file_silo_plugin_v1_request_router_proto_rawDesc = "" + "\n" + - "#silo/plugin/v1/request_router.proto\x12\x0esilo.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xc4\x03\n" + + "#silo/plugin/v1/request_router.proto\x12\x0esilo.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xde\x03\n" + "\x11RequestDescriptor\x12\x1d\n" + "\n" + "media_type\x18\x01 \x01(\tR\tmediaType\x12\x14\n" + @@ -1143,10 +1211,14 @@ const file_silo_plugin_v1_request_router_proto_rawDesc = "" + "\x11requester_user_id\x18\x06 \x01(\x03R\x0frequesterUserId\x120\n" + "\x14requester_profile_id\x18\a \x01(\tR\x12requesterProfileId\x12'\n" + "\x0frequester_email\x18\b \x01(\tR\x0erequesterEmail\x12-\n" + - "\x12requester_username\x18\t \x01(\tR\x11requesterUsername\x1a>\n" + + "\x12requester_username\x18\t \x01(\tR\x11requesterUsername\x12\x18\n" + + "\aseasons\x18\n" + + " \x03(\x05R\aseasons\x1a>\n" + "\x10ExternalIdsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x87\x01\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"D\n" + + "\x17RequestRouterDescriptor\x12)\n" + + "\x10supports_seasons\x18\x01 \x01(\bR\x0fsupportsSeasons\"\x87\x01\n" + "\x10RouterConnection\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x19\n" + "\bbase_url\x18\x02 \x01(\tR\abaseUrl\x12\x17\n" + @@ -1244,60 +1316,61 @@ func file_silo_plugin_v1_request_router_proto_rawDescGZIP() []byte { return file_silo_plugin_v1_request_router_proto_rawDescData } -var file_silo_plugin_v1_request_router_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_silo_plugin_v1_request_router_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_silo_plugin_v1_request_router_proto_goTypes = []any{ (*RequestDescriptor)(nil), // 0: silo.plugin.v1.RequestDescriptor - (*RouterConnection)(nil), // 1: silo.plugin.v1.RouterConnection - (*RequestedQuality)(nil), // 2: silo.plugin.v1.RequestedQuality - (*FulfillRequest)(nil), // 3: silo.plugin.v1.FulfillRequest - (*FulfillmentTarget)(nil), // 4: silo.plugin.v1.FulfillmentTarget - (*FulfillResponse)(nil), // 5: silo.plugin.v1.FulfillResponse - (*TargetRef)(nil), // 6: silo.plugin.v1.TargetRef - (*CheckStatusRequest)(nil), // 7: silo.plugin.v1.CheckStatusRequest - (*TargetStatus)(nil), // 8: silo.plugin.v1.TargetStatus - (*CheckStatusResponse)(nil), // 9: silo.plugin.v1.CheckStatusResponse - (*ConfigOption)(nil), // 10: silo.plugin.v1.ConfigOption - (*ConfigOptionList)(nil), // 11: silo.plugin.v1.ConfigOptionList - (*ListConfigOptionsRequest)(nil), // 12: silo.plugin.v1.ListConfigOptionsRequest - (*ListConfigOptionsResponse)(nil), // 13: silo.plugin.v1.ListConfigOptionsResponse - (*TestConnectionRequest)(nil), // 14: silo.plugin.v1.TestConnectionRequest - (*TestConnectionResponse)(nil), // 15: silo.plugin.v1.TestConnectionResponse - (*ValidateRequest)(nil), // 16: silo.plugin.v1.ValidateRequest - (*ValidateResponse)(nil), // 17: silo.plugin.v1.ValidateResponse - nil, // 18: silo.plugin.v1.RequestDescriptor.ExternalIdsEntry - nil, // 19: silo.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry - nil, // 20: silo.plugin.v1.ValidateResponse.FieldErrorsEntry - (*structpb.Struct)(nil), // 21: google.protobuf.Struct + (*RequestRouterDescriptor)(nil), // 1: silo.plugin.v1.RequestRouterDescriptor + (*RouterConnection)(nil), // 2: silo.plugin.v1.RouterConnection + (*RequestedQuality)(nil), // 3: silo.plugin.v1.RequestedQuality + (*FulfillRequest)(nil), // 4: silo.plugin.v1.FulfillRequest + (*FulfillmentTarget)(nil), // 5: silo.plugin.v1.FulfillmentTarget + (*FulfillResponse)(nil), // 6: silo.plugin.v1.FulfillResponse + (*TargetRef)(nil), // 7: silo.plugin.v1.TargetRef + (*CheckStatusRequest)(nil), // 8: silo.plugin.v1.CheckStatusRequest + (*TargetStatus)(nil), // 9: silo.plugin.v1.TargetStatus + (*CheckStatusResponse)(nil), // 10: silo.plugin.v1.CheckStatusResponse + (*ConfigOption)(nil), // 11: silo.plugin.v1.ConfigOption + (*ConfigOptionList)(nil), // 12: silo.plugin.v1.ConfigOptionList + (*ListConfigOptionsRequest)(nil), // 13: silo.plugin.v1.ListConfigOptionsRequest + (*ListConfigOptionsResponse)(nil), // 14: silo.plugin.v1.ListConfigOptionsResponse + (*TestConnectionRequest)(nil), // 15: silo.plugin.v1.TestConnectionRequest + (*TestConnectionResponse)(nil), // 16: silo.plugin.v1.TestConnectionResponse + (*ValidateRequest)(nil), // 17: silo.plugin.v1.ValidateRequest + (*ValidateResponse)(nil), // 18: silo.plugin.v1.ValidateResponse + nil, // 19: silo.plugin.v1.RequestDescriptor.ExternalIdsEntry + nil, // 20: silo.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry + nil, // 21: silo.plugin.v1.ValidateResponse.FieldErrorsEntry + (*structpb.Struct)(nil), // 22: google.protobuf.Struct } var file_silo_plugin_v1_request_router_proto_depIdxs = []int32{ - 18, // 0: silo.plugin.v1.RequestDescriptor.external_ids:type_name -> silo.plugin.v1.RequestDescriptor.ExternalIdsEntry - 21, // 1: silo.plugin.v1.RouterConnection.config:type_name -> google.protobuf.Struct + 19, // 0: silo.plugin.v1.RequestDescriptor.external_ids:type_name -> silo.plugin.v1.RequestDescriptor.ExternalIdsEntry + 22, // 1: silo.plugin.v1.RouterConnection.config:type_name -> google.protobuf.Struct 0, // 2: silo.plugin.v1.FulfillRequest.request:type_name -> silo.plugin.v1.RequestDescriptor - 2, // 3: silo.plugin.v1.FulfillRequest.qualities:type_name -> silo.plugin.v1.RequestedQuality - 1, // 4: silo.plugin.v1.FulfillRequest.connections:type_name -> silo.plugin.v1.RouterConnection - 4, // 5: silo.plugin.v1.FulfillResponse.targets:type_name -> silo.plugin.v1.FulfillmentTarget + 3, // 3: silo.plugin.v1.FulfillRequest.qualities:type_name -> silo.plugin.v1.RequestedQuality + 2, // 4: silo.plugin.v1.FulfillRequest.connections:type_name -> silo.plugin.v1.RouterConnection + 5, // 5: silo.plugin.v1.FulfillResponse.targets:type_name -> silo.plugin.v1.FulfillmentTarget 0, // 6: silo.plugin.v1.CheckStatusRequest.request:type_name -> silo.plugin.v1.RequestDescriptor - 6, // 7: silo.plugin.v1.CheckStatusRequest.targets:type_name -> silo.plugin.v1.TargetRef - 1, // 8: silo.plugin.v1.CheckStatusRequest.connections:type_name -> silo.plugin.v1.RouterConnection - 8, // 9: silo.plugin.v1.CheckStatusResponse.statuses:type_name -> silo.plugin.v1.TargetStatus - 10, // 10: silo.plugin.v1.ConfigOptionList.options:type_name -> silo.plugin.v1.ConfigOption - 1, // 11: silo.plugin.v1.ListConfigOptionsRequest.connection:type_name -> silo.plugin.v1.RouterConnection - 19, // 12: silo.plugin.v1.ListConfigOptionsResponse.options_by_field:type_name -> silo.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry - 1, // 13: silo.plugin.v1.TestConnectionRequest.connection:type_name -> silo.plugin.v1.RouterConnection - 1, // 14: silo.plugin.v1.ValidateRequest.connection:type_name -> silo.plugin.v1.RouterConnection - 1, // 15: silo.plugin.v1.ValidateRequest.siblings:type_name -> silo.plugin.v1.RouterConnection - 20, // 16: silo.plugin.v1.ValidateResponse.field_errors:type_name -> silo.plugin.v1.ValidateResponse.FieldErrorsEntry - 11, // 17: silo.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry.value:type_name -> silo.plugin.v1.ConfigOptionList - 3, // 18: silo.plugin.v1.RequestRouter.Fulfill:input_type -> silo.plugin.v1.FulfillRequest - 7, // 19: silo.plugin.v1.RequestRouter.CheckStatus:input_type -> silo.plugin.v1.CheckStatusRequest - 12, // 20: silo.plugin.v1.RequestRouter.ListConfigOptions:input_type -> silo.plugin.v1.ListConfigOptionsRequest - 14, // 21: silo.plugin.v1.RequestRouter.TestConnection:input_type -> silo.plugin.v1.TestConnectionRequest - 16, // 22: silo.plugin.v1.RequestRouter.Validate:input_type -> silo.plugin.v1.ValidateRequest - 5, // 23: silo.plugin.v1.RequestRouter.Fulfill:output_type -> silo.plugin.v1.FulfillResponse - 9, // 24: silo.plugin.v1.RequestRouter.CheckStatus:output_type -> silo.plugin.v1.CheckStatusResponse - 13, // 25: silo.plugin.v1.RequestRouter.ListConfigOptions:output_type -> silo.plugin.v1.ListConfigOptionsResponse - 15, // 26: silo.plugin.v1.RequestRouter.TestConnection:output_type -> silo.plugin.v1.TestConnectionResponse - 17, // 27: silo.plugin.v1.RequestRouter.Validate:output_type -> silo.plugin.v1.ValidateResponse + 7, // 7: silo.plugin.v1.CheckStatusRequest.targets:type_name -> silo.plugin.v1.TargetRef + 2, // 8: silo.plugin.v1.CheckStatusRequest.connections:type_name -> silo.plugin.v1.RouterConnection + 9, // 9: silo.plugin.v1.CheckStatusResponse.statuses:type_name -> silo.plugin.v1.TargetStatus + 11, // 10: silo.plugin.v1.ConfigOptionList.options:type_name -> silo.plugin.v1.ConfigOption + 2, // 11: silo.plugin.v1.ListConfigOptionsRequest.connection:type_name -> silo.plugin.v1.RouterConnection + 20, // 12: silo.plugin.v1.ListConfigOptionsResponse.options_by_field:type_name -> silo.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry + 2, // 13: silo.plugin.v1.TestConnectionRequest.connection:type_name -> silo.plugin.v1.RouterConnection + 2, // 14: silo.plugin.v1.ValidateRequest.connection:type_name -> silo.plugin.v1.RouterConnection + 2, // 15: silo.plugin.v1.ValidateRequest.siblings:type_name -> silo.plugin.v1.RouterConnection + 21, // 16: silo.plugin.v1.ValidateResponse.field_errors:type_name -> silo.plugin.v1.ValidateResponse.FieldErrorsEntry + 12, // 17: silo.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry.value:type_name -> silo.plugin.v1.ConfigOptionList + 4, // 18: silo.plugin.v1.RequestRouter.Fulfill:input_type -> silo.plugin.v1.FulfillRequest + 8, // 19: silo.plugin.v1.RequestRouter.CheckStatus:input_type -> silo.plugin.v1.CheckStatusRequest + 13, // 20: silo.plugin.v1.RequestRouter.ListConfigOptions:input_type -> silo.plugin.v1.ListConfigOptionsRequest + 15, // 21: silo.plugin.v1.RequestRouter.TestConnection:input_type -> silo.plugin.v1.TestConnectionRequest + 17, // 22: silo.plugin.v1.RequestRouter.Validate:input_type -> silo.plugin.v1.ValidateRequest + 6, // 23: silo.plugin.v1.RequestRouter.Fulfill:output_type -> silo.plugin.v1.FulfillResponse + 10, // 24: silo.plugin.v1.RequestRouter.CheckStatus:output_type -> silo.plugin.v1.CheckStatusResponse + 14, // 25: silo.plugin.v1.RequestRouter.ListConfigOptions:output_type -> silo.plugin.v1.ListConfigOptionsResponse + 16, // 26: silo.plugin.v1.RequestRouter.TestConnection:output_type -> silo.plugin.v1.TestConnectionResponse + 18, // 27: silo.plugin.v1.RequestRouter.Validate:output_type -> silo.plugin.v1.ValidateResponse 23, // [23:28] is the sub-list for method output_type 18, // [18:23] is the sub-list for method input_type 18, // [18:18] is the sub-list for extension type_name @@ -1316,7 +1389,7 @@ func file_silo_plugin_v1_request_router_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_silo_plugin_v1_request_router_proto_rawDesc), len(file_silo_plugin_v1_request_router_proto_rawDesc)), NumEnums: 0, - NumMessages: 21, + NumMessages: 22, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/pluginsdk/convert/convert.go b/pkg/pluginsdk/convert/convert.go index 1e4fd5c..93c324c 100644 --- a/pkg/pluginsdk/convert/convert.go +++ b/pkg/pluginsdk/convert/convert.go @@ -81,6 +81,17 @@ func DecodeCapability(record CapabilityRecord) (*pluginv1.CapabilityDescriptor, } descriptor.WatchSyncProvider = &typed } + if requestRouter, ok := record.Metadata["request_router"]; ok { + data, err := json.Marshal(requestRouter) + if err != nil { + return nil, fmt.Errorf("encode request router descriptor: %w", err) + } + var typed pluginv1.RequestRouterDescriptor + if err := capabilityUnmarshal.Unmarshal(data, &typed); err != nil { + return nil, fmt.Errorf("decode request router descriptor: %w", err) + } + descriptor.RequestRouter = &typed + } if configSchema, ok := record.Metadata["config_schema"]; ok { schemas, err := decodeConfigSchemas(configSchema) if err != nil { @@ -128,6 +139,17 @@ func capabilityMetadata(descriptor *pluginv1.CapabilityDescriptor) (map[string]a } metadata["watch_sync_provider"] = value } + if descriptor.GetRequestRouter() != nil { + data, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(descriptor.GetRequestRouter()) + if err != nil { + return nil, fmt.Errorf("encode request router descriptor: %w", err) + } + var value map[string]any + if err := json.Unmarshal(data, &value); err != nil { + return nil, fmt.Errorf("decode request router descriptor JSON: %w", err) + } + metadata["request_router"] = value + } if len(descriptor.GetConfigSchema()) > 0 { schemas := make([]map[string]any, 0, len(descriptor.GetConfigSchema())) for _, schema := range descriptor.GetConfigSchema() { diff --git a/pkg/pluginsdk/convert/request_router_test.go b/pkg/pluginsdk/convert/request_router_test.go new file mode 100644 index 0000000..f1d4e6d --- /dev/null +++ b/pkg/pluginsdk/convert/request_router_test.go @@ -0,0 +1,61 @@ +package convert_test + +import ( + "testing" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" + "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/convert" +) + +func TestRequestRouterDescriptorRoundTrip(t *testing.T) { + manifest := &pluginv1.PluginManifest{Capabilities: []*pluginv1.CapabilityDescriptor{{ + Type: "request_router.v1", + Id: "arr", + RequestRouter: &pluginv1.RequestRouterDescriptor{SupportsSeasons: true}, + }}} + records, err := convert.CapabilityRecordsFromManifest(manifest) + if err != nil { + t.Fatal(err) + } + decoded, err := convert.DecodeCapability(records[0]) + if err != nil { + t.Fatal(err) + } + if !decoded.GetRequestRouter().GetSupportsSeasons() { + t.Fatalf("decoded descriptor = %#v, want supports_seasons", decoded.GetRequestRouter()) + } +} + +// Metadata stored for a request router built on an older SDK has no +// request_router key; it must decode as declaring no season support. +func TestDecodeCapabilityWithoutRequestRouterDeclaresNoSeasonSupport(t *testing.T) { + record := convert.CapabilityRecord{ + Type: "request_router.v1", + ID: "arr", + Metadata: map[string]any{"display_name": "Arr"}, + } + decoded, err := convert.DecodeCapability(record) + if err != nil { + t.Fatal(err) + } + if decoded.GetRequestRouter() != nil { + t.Fatalf("decoded descriptor = %#v, want absent", decoded.GetRequestRouter()) + } +} + +func TestDecodeCapabilityDiscardsUnknownRequestRouterData(t *testing.T) { + record := convert.CapabilityRecord{ + Type: "request_router.v1", + ID: "arr", + Metadata: map[string]any{ + "request_router": map[string]any{"supports_seasons": true, "supports_future_thing": true}, + }, + } + decoded, err := convert.DecodeCapability(record) + if err != nil { + t.Fatalf("DecodeCapability() error = %v", err) + } + if !decoded.GetRequestRouter().GetSupportsSeasons() { + t.Fatalf("decoded descriptor = %#v, want supports_seasons", decoded.GetRequestRouter()) + } +} diff --git a/pkg/pluginsdk/manifest/manifest.go b/pkg/pluginsdk/manifest/manifest.go index 0da82bd..746eb43 100644 --- a/pkg/pluginsdk/manifest/manifest.go +++ b/pkg/pluginsdk/manifest/manifest.go @@ -116,6 +116,9 @@ func Validate(manifest *pluginv1.PluginManifest) error { if err := validateNetworkAccessCapability(capability); err != nil { return err } + if err := validateRequestRouterCapability(capability); err != nil { + return err + } } for _, schema := range manifest.GlobalConfigSchema { if err := validateConfigSchema(schema); err != nil { @@ -229,6 +232,15 @@ func validateNetworkAccessCapability(descriptor *pluginv1.CapabilityDescriptor) return nil } +// validateRequestRouterCapability keeps the descriptor optional so request +// routers built before it existed stay valid. +func validateRequestRouterCapability(descriptor *pluginv1.CapabilityDescriptor) error { + if descriptor.GetType() != capability.RequestRouter && descriptor.GetRequestRouter() != nil { + return fmt.Errorf("plugin capability %q: request_router descriptor requires type %q", descriptor.GetId(), capability.RequestRouter) + } + return nil +} + // ValidateCatalogPresentation applies the stricter presentation contract used // by curated catalogs. The general manifest validator keeps presentation // optional so older and directly uploaded plugins remain compatible. diff --git a/pkg/pluginsdk/manifest/request_router_test.go b/pkg/pluginsdk/manifest/request_router_test.go new file mode 100644 index 0000000..53ec6b8 --- /dev/null +++ b/pkg/pluginsdk/manifest/request_router_test.go @@ -0,0 +1,55 @@ +package manifest_test + +import ( + "testing" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" + publicmanifest "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/manifest" +) + +func TestLoadRequestRouterDeclaresSeasonSupport(t *testing.T) { + raw := []byte(`{ + "plugin_id":"silo.requests-arr", "version":"1.0.0", "silo_api_version":"v1", + "capabilities":[{ + "type":"request_router.v1", "id":"arr", "display_name":"Arr", + "request_router":{"supports_seasons":true} + }] + }`) + manifest, err := publicmanifest.Load(raw) + if err != nil { + t.Fatal(err) + } + if !manifest.GetCapabilities()[0].GetRequestRouter().GetSupportsSeasons() { + t.Fatal("supports_seasons = false, want true") + } +} + +// A manifest written before the descriptor existed stays valid and declares no +// season support, so the host keeps today's whole-series behaviour. +func TestLoadRequestRouterWithoutDescriptorDeclaresNoSeasonSupport(t *testing.T) { + raw := []byte(`{ + "plugin_id":"silo.requests-arr", "version":"1.0.0", "silo_api_version":"v1", + "capabilities":[{"type":"request_router.v1", "id":"arr", "display_name":"Arr"}] + }`) + manifest, err := publicmanifest.Load(raw) + if err != nil { + t.Fatal(err) + } + capability := manifest.GetCapabilities()[0] + if capability.GetRequestRouter() != nil { + t.Fatalf("request router descriptor = %#v, want absent", capability.GetRequestRouter()) + } +} + +func TestValidateRequestRouterRejectsDescriptorOnOtherType(t *testing.T) { + manifest := &pluginv1.PluginManifest{ + PluginId: "silo.invalid", Version: "1.0.0", + Capabilities: []*pluginv1.CapabilityDescriptor{{ + Type: "scheduled_task.v1", Id: "task", + RequestRouter: &pluginv1.RequestRouterDescriptor{SupportsSeasons: true}, + }}, + } + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected request router descriptor on a scheduled task to fail") + } +} diff --git a/pkg/pluginsdk/runtime/request_router_test.go b/pkg/pluginsdk/runtime/request_router_test.go new file mode 100644 index 0000000..adca8aa --- /dev/null +++ b/pkg/pluginsdk/runtime/request_router_test.go @@ -0,0 +1,57 @@ +package runtime_test + +import ( + "context" + "net" + "slices" + "testing" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" + runtime "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/runtime" + "github.com/hashicorp/go-plugin" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" +) + +type recordingRequestRouter struct { + pluginv1.UnimplementedRequestRouterServer + seasons chan []int32 +} + +func (r recordingRequestRouter) Fulfill(_ context.Context, req *pluginv1.FulfillRequest) (*pluginv1.FulfillResponse, error) { + r.seasons <- req.GetRequest().GetSeasons() + return &pluginv1.FulfillResponse{}, nil +} + +func TestRequestRouterReceivesRequestedSeasons(t *testing.T) { + router := recordingRequestRouter{seasons: make(chan []int32, 1)} + plugins := runtime.DefaultPluginSet(runtime.CapabilityServers{Runtime: stubRuntime{}, RequestRouter: router}) + p := plugins[runtime.PluginSetName].(plugin.GRPCPlugin) + srv := grpc.NewServer() + if err := p.GRPCServer(nil, srv); err != nil { + t.Fatalf("GRPCServer = %v, want nil", err) + } + listener := bufconn.Listen(1 << 20) + go func() { _ = srv.Serve(listener) }() + t.Cleanup(srv.Stop) + + conn, err := grpc.NewClient("passthrough:///bufnet", + grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return listener.DialContext(ctx) }), + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("NewClient = %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + + _, err = pluginv1.NewRequestRouterClient(conn).Fulfill(context.Background(), &pluginv1.FulfillRequest{ + CapabilityId: "arr", + Request: &pluginv1.RequestDescriptor{MediaType: "series", Title: "Example", Seasons: []int32{0, 4}}, + }) + if err != nil { + t.Fatalf("Fulfill = %v", err) + } + if got := <-router.seasons; !slices.Equal(got, []int32{0, 4}) { + t.Fatalf("seasons = %v, want [0 4]", got) + } +} diff --git a/proto/silo/plugin/v1/common.proto b/proto/silo/plugin/v1/common.proto index dae0ff9..2507ca6 100644 --- a/proto/silo/plugin/v1/common.proto +++ b/proto/silo/plugin/v1/common.proto @@ -6,6 +6,7 @@ option go_package = "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo import "google/protobuf/struct.proto"; import "silo/plugin/v1/network_access_provider.proto"; +import "silo/plugin/v1/request_router.proto"; import "silo/plugin/v1/watch_sync_provider.proto"; message SupportedPlatform { @@ -148,6 +149,11 @@ message CapabilityDescriptor { // of type "network_access_provider.v1". The host lists available overlay // providers from this descriptor without launching the plugin. NetworkAccessProviderDescriptor network_access_provider = 11; + + // Typed request-router contract metadata. Only meaningful for capabilities + // of type "request_router.v1". Optional: an absent descriptor declares no + // optional request-router features. + RequestRouterDescriptor request_router = 12; } // PluginPresentation describes a plugin for server operators and links them to diff --git a/proto/silo/plugin/v1/request_router.proto b/proto/silo/plugin/v1/request_router.proto index 309a707..ca23728 100644 --- a/proto/silo/plugin/v1/request_router.proto +++ b/proto/silo/plugin/v1/request_router.proto @@ -31,6 +31,27 @@ message RequestDescriptor { // on the downstream service (e.g. Seerr). Empty when the host cannot resolve it. string requester_email = 8; string requester_username = 9; + // seasons lists the requested season numbers of a series; 0 is Specials. + // Empty means the whole series, which is what every request meant before + // this field existed. Only set when media_type is "series". Plugins that + // declare RequestRouterDescriptor.supports_seasons must fulfil only these + // seasons; see that field for the full contract. + repeated int32 seasons = 10; +} + +// RequestRouterDescriptor is typed request_router.v1 contract metadata that the +// host reads from the manifest without launching the plugin. An absent +// descriptor means every flag is false, which is how the host treats plugins +// built before a flag existed. +message RequestRouterDescriptor { + // supports_seasons declares that Fulfill and CheckStatus honour + // RequestDescriptor.seasons. A declaring plugin acquires only the listed + // seasons; when the series already exists upstream it adds those seasons to + // what is already tracked and leaves other seasons unchanged. Repeating a + // request converges rather than duplicating work. The host sends a request + // for only the missing seasons of a series it already has only to plugins + // that set this flag, because other plugins would add the whole series. + bool supports_seasons = 1; } message RouterConnection { From b7573c37167c3eb1e5e76f124f7b344eedc236ed Mon Sep 17 00:00:00 2001 From: JonahMMay Date: Mon, 28 Sep 2026 14:49:15 +0000 Subject: [PATCH 17/17] docs: point CONTRIBUTING at the prairie-server contribution guide The Prairie-Server/.github repository is empty, so the upstream-style link to its CONTRIBUTING.md is dead. Link the project-wide guide in prairie-server instead. Co-Authored-By: Claude Opus 5.5 (1M context) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 21e7001..e2e2361 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to the Prairie Plugin SDK -The [Prairie contribution guide](https://github.com/prairie-server/.github/blob/main/CONTRIBUTING.md) +The [Prairie contribution guide](https://github.com/prairie-server/prairie-server/blob/main/CONTRIBUTING.md) covers project-wide coordination, focused changes, evidence, AI disclosure, and pull request expectations. Those requirements apply here; this guide adds the SDK-specific workflow.