From 613e3bf5dc0e6d7fa322059cf7442fc7faed0520 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:13:05 -0400 Subject: [PATCH 1/3] feat(proto): report the language of metadata overviews and child text (#49) MetadataItem gains overview_language, and SeasonRecord and EpisodeRecord gain title_language and overview_language. A plugin sets them when the provider had no text in the requested language and it returned another language's text, so the host can rank it below text in the requested language and fill it from a fallback language. Empty keeps today's meaning. Co-authored-by: Claude Opus 5.5 --- docs/compatibility.md | 11 +++ .../silo/plugin/v1/metadata_provider.pb.go | 96 ++++++++++++++----- .../silo/plugin/v1/metadata_provider_test.go | 8 ++ proto/silo/plugin/v1/metadata_provider.proto | 12 +++ 4 files changed, 105 insertions(+), 22 deletions(-) diff --git a/docs/compatibility.md b/docs/compatibility.md index 1186460..166417c 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -179,6 +179,17 @@ metadata stored when the plugin was installed or updated. A host built on an older SDK stored that metadata without them, so they take effect after the plugin is next updated on a host that reads them. +`MetadataItem.overview_language` and the `title_language` and +`overview_language` fields on `SeasonRecord` and `EpisodeRecord` (added in +v0.29.0) are plain strings, like the older `MetadataItem.title_language`. +Empty means the text is in the requested language, or the plugin cannot tell; +hosts read every plugin built before the fields existed that way. A plugin +sets one when the provider had no text in the requested language and it +returned another language's text instead, such as a record's original-language +overview. Hosts rank that text below another provider's text in the requested +language and may replace it with text fetched in a fallback language. Hosts +that predate the fields ignore them. + 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/metadata_provider.pb.go b/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go index 4afebbd..3df7e3b 100644 --- a/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/metadata_provider.pb.go @@ -620,8 +620,12 @@ type MetadataItem struct { // this provider response. Older plugins decode as false, so hosts must merge // rather than delete aliases when completeness is unknown. TitleAliasesComplete bool `protobuf:"varint,36,opt,name=title_aliases_complete,json=titleAliasesComplete,proto3" json:"title_aliases_complete,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // ISO 639-1 language of overview and tagline. Empty means the text is in the + // requested language or the plugin cannot tell; set it when the provider + // had no text in the requested language and returned another language's. + OverviewLanguage string `protobuf:"bytes,37,opt,name=overview_language,json=overviewLanguage,proto3" json:"overview_language,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *MetadataItem) Reset() { @@ -907,6 +911,13 @@ func (x *MetadataItem) GetTitleAliasesComplete() bool { return false } +func (x *MetadataItem) GetOverviewLanguage() string { + if x != nil { + return x.OverviewLanguage + } + return "" +} + type GetMetadataRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ProviderId string `protobuf:"bytes,1,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` @@ -1240,17 +1251,21 @@ func (x *GetPersonDetailResponse) GetPerson() *PersonDetailRecord { } type SeasonRecord struct { - state protoimpl.MessageState `protogen:"open.v1"` - SeasonNumber int32 `protobuf:"varint,1,opt,name=season_number,json=seasonNumber,proto3" json:"season_number,omitempty"` - Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` - Overview string `protobuf:"bytes,3,opt,name=overview,proto3" json:"overview,omitempty"` - AirDate string `protobuf:"bytes,4,opt,name=air_date,json=airDate,proto3" json:"air_date,omitempty"` - PosterPath string `protobuf:"bytes,5,opt,name=poster_path,json=posterPath,proto3" json:"poster_path,omitempty"` - Metadata *structpb.Struct `protobuf:"bytes,6,opt,name=metadata,proto3" json:"metadata,omitempty"` - ProviderId string `protobuf:"bytes,7,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` - ProviderIds *structpb.Struct `protobuf:"bytes,8,opt,name=provider_ids,json=providerIds,proto3" json:"provider_ids,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + SeasonNumber int32 `protobuf:"varint,1,opt,name=season_number,json=seasonNumber,proto3" json:"season_number,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Overview string `protobuf:"bytes,3,opt,name=overview,proto3" json:"overview,omitempty"` + AirDate string `protobuf:"bytes,4,opt,name=air_date,json=airDate,proto3" json:"air_date,omitempty"` + PosterPath string `protobuf:"bytes,5,opt,name=poster_path,json=posterPath,proto3" json:"poster_path,omitempty"` + Metadata *structpb.Struct `protobuf:"bytes,6,opt,name=metadata,proto3" json:"metadata,omitempty"` + ProviderId string `protobuf:"bytes,7,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` + ProviderIds *structpb.Struct `protobuf:"bytes,8,opt,name=provider_ids,json=providerIds,proto3" json:"provider_ids,omitempty"` + // ISO 639-1 languages of title and overview, with MetadataItem's + // overview_language semantics: empty means the requested language or unknown. + TitleLanguage string `protobuf:"bytes,9,opt,name=title_language,json=titleLanguage,proto3" json:"title_language,omitempty"` + OverviewLanguage string `protobuf:"bytes,10,opt,name=overview_language,json=overviewLanguage,proto3" json:"overview_language,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SeasonRecord) Reset() { @@ -1339,6 +1354,20 @@ func (x *SeasonRecord) GetProviderIds() *structpb.Struct { return nil } +func (x *SeasonRecord) GetTitleLanguage() string { + if x != nil { + return x.TitleLanguage + } + return "" +} + +func (x *SeasonRecord) GetOverviewLanguage() string { + if x != nil { + return x.OverviewLanguage + } + return "" +} + type GetSeasonsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` SeriesProviderId string `protobuf:"bytes,1,opt,name=series_provider_id,json=seriesProviderId,proto3" json:"series_provider_id,omitempty"` @@ -1456,8 +1485,12 @@ type EpisodeRecord struct { Ratings *structpb.Struct `protobuf:"bytes,9,opt,name=ratings,proto3" json:"ratings,omitempty"` Metadata *structpb.Struct `protobuf:"bytes,10,opt,name=metadata,proto3" json:"metadata,omitempty"` ProviderId string `protobuf:"bytes,11,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // ISO 639-1 languages of title and overview, with MetadataItem's + // overview_language semantics: empty means the requested language or unknown. + TitleLanguage string `protobuf:"bytes,12,opt,name=title_language,json=titleLanguage,proto3" json:"title_language,omitempty"` + OverviewLanguage string `protobuf:"bytes,13,opt,name=overview_language,json=overviewLanguage,proto3" json:"overview_language,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *EpisodeRecord) Reset() { @@ -1567,6 +1600,20 @@ func (x *EpisodeRecord) GetProviderId() string { return "" } +func (x *EpisodeRecord) GetTitleLanguage() string { + if x != nil { + return x.TitleLanguage + } + return "" +} + +func (x *EpisodeRecord) GetOverviewLanguage() string { + if x != nil { + return x.OverviewLanguage + } + return "" +} + type GetEpisodesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` SeriesProviderId string `protobuf:"bytes,1,opt,name=series_provider_id,json=seriesProviderId,proto3" json:"series_provider_id,omitempty"` @@ -2161,8 +2208,7 @@ const file_silo_plugin_v1_metadata_provider_proto_rawDesc = "" + "\vis_official\x18\a \x01(\bR\n" + "isOfficial\x12\x1b\n" + "\tsize_hint\x18\b \x01(\x05R\bsizeHint\x12!\n" + - "\fpublished_at\x18\t \x01(\tR\vpublishedAt\"\xe7\n" + - "\n" + + "\fpublished_at\x18\t \x01(\tR\vpublishedAt\"\x94\v\n" + "\fMetadataItem\x12\x1f\n" + "\vprovider_id\x18\x01 \x01(\tR\n" + "providerId\x12\x1b\n" + @@ -2203,7 +2249,8 @@ const file_silo_plugin_v1_metadata_provider_proto_rawDesc = "" + "\rtitle_aliases\x18! \x03(\v2\x1a.silo.plugin.v1.TitleAliasR\ftitleAliases\x12%\n" + "\x0etitle_language\x18\" \x01(\tR\rtitleLanguage\x12*\n" + "\x11title_is_fallback\x18# \x01(\bR\x0ftitleIsFallback\x124\n" + - "\x16title_aliases_complete\x18$ \x01(\bR\x14titleAliasesComplete\"\xc7\x01\n" + + "\x16title_aliases_complete\x18$ \x01(\bR\x14titleAliasesComplete\x12+\n" + + "\x11overview_language\x18% \x01(\tR\x10overviewLanguage\"\xc7\x01\n" + "\x12GetMetadataRequest\x12\x1f\n" + "\vprovider_id\x18\x01 \x01(\tR\n" + "providerId\x12\x1b\n" + @@ -2234,7 +2281,7 @@ const file_silo_plugin_v1_metadata_provider_proto_rawDesc = "" + "\fprovider_ids\x18\n" + " \x01(\v2\x17.google.protobuf.StructR\vproviderIds\"U\n" + "\x17GetPersonDetailResponse\x12:\n" + - "\x06person\x18\x01 \x01(\v2\".silo.plugin.v1.PersonDetailRecordR\x06person\"\xb3\x02\n" + + "\x06person\x18\x01 \x01(\v2\".silo.plugin.v1.PersonDetailRecordR\x06person\"\x87\x03\n" + "\fSeasonRecord\x12#\n" + "\rseason_number\x18\x01 \x01(\x05R\fseasonNumber\x12\x14\n" + "\x05title\x18\x02 \x01(\tR\x05title\x12\x1a\n" + @@ -2245,13 +2292,16 @@ const file_silo_plugin_v1_metadata_provider_proto_rawDesc = "" + "\bmetadata\x18\x06 \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12\x1f\n" + "\vprovider_id\x18\a \x01(\tR\n" + "providerId\x12:\n" + - "\fprovider_ids\x18\b \x01(\v2\x17.google.protobuf.StructR\vproviderIds\"\x99\x01\n" + + "\fprovider_ids\x18\b \x01(\v2\x17.google.protobuf.StructR\vproviderIds\x12%\n" + + "\x0etitle_language\x18\t \x01(\tR\rtitleLanguage\x12+\n" + + "\x11overview_language\x18\n" + + " \x01(\tR\x10overviewLanguage\"\x99\x01\n" + "\x11GetSeasonsRequest\x12,\n" + "\x12series_provider_id\x18\x01 \x01(\tR\x10seriesProviderId\x12:\n" + "\fprovider_ids\x18\x02 \x01(\v2\x17.google.protobuf.StructR\vproviderIds\x12\x1a\n" + "\blanguage\x18\x03 \x01(\tR\blanguage\"L\n" + "\x12GetSeasonsResponse\x126\n" + - "\aseasons\x18\x01 \x03(\v2\x1c.silo.plugin.v1.SeasonRecordR\aseasons\"\xa6\x03\n" + + "\aseasons\x18\x01 \x03(\v2\x1c.silo.plugin.v1.SeasonRecordR\aseasons\"\xfa\x03\n" + "\rEpisodeRecord\x12#\n" + "\rseason_number\x18\x01 \x01(\x05R\fseasonNumber\x12%\n" + "\x0eepisode_number\x18\x02 \x01(\x05R\repisodeNumber\x12\x14\n" + @@ -2266,7 +2316,9 @@ const file_silo_plugin_v1_metadata_provider_proto_rawDesc = "" + "\bmetadata\x18\n" + " \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12\x1f\n" + "\vprovider_id\x18\v \x01(\tR\n" + - "providerId\"\xbf\x01\n" + + "providerId\x12%\n" + + "\x0etitle_language\x18\f \x01(\tR\rtitleLanguage\x12+\n" + + "\x11overview_language\x18\r \x01(\tR\x10overviewLanguage\"\xbf\x01\n" + "\x12GetEpisodesRequest\x12,\n" + "\x12series_provider_id\x18\x01 \x01(\tR\x10seriesProviderId\x12#\n" + "\rseason_number\x18\x02 \x01(\x05R\fseasonNumber\x12:\n" + diff --git a/pkg/pluginproto/silo/plugin/v1/metadata_provider_test.go b/pkg/pluginproto/silo/plugin/v1/metadata_provider_test.go index f47c4f8..89d5371 100644 --- a/pkg/pluginproto/silo/plugin/v1/metadata_provider_test.go +++ b/pkg/pluginproto/silo/plugin/v1/metadata_provider_test.go @@ -56,6 +56,14 @@ func TestMetadataTitleDescriptors_IncludeAliasAndLanguageFields(t *testing.T) { assertFieldNumber(t, &MetadataItem{}, "title_aliases_complete", 36) } +func TestMetadataTextLanguageFieldNumbers(t *testing.T) { + assertFieldNumber(t, &MetadataItem{}, "overview_language", 37) + assertFieldNumber(t, &SeasonRecord{}, "title_language", 9) + assertFieldNumber(t, &SeasonRecord{}, "overview_language", 10) + assertFieldNumber(t, &EpisodeRecord{}, "title_language", 12) + assertFieldNumber(t, &EpisodeRecord{}, "overview_language", 13) +} + func TestMetadataItem_OldPayloadDecodesWithOptionalTitleFields(t *testing.T) { oldPayload, err := proto.Marshal(&MetadataItem{ ProviderId: "603", ItemType: "movie", Title: "The Matrix", OriginalTitle: "The Matrix", Year: 1999, diff --git a/proto/silo/plugin/v1/metadata_provider.proto b/proto/silo/plugin/v1/metadata_provider.proto index 05b3eef..bbf117b 100644 --- a/proto/silo/plugin/v1/metadata_provider.proto +++ b/proto/silo/plugin/v1/metadata_provider.proto @@ -140,6 +140,10 @@ message MetadataItem { // this provider response. Older plugins decode as false, so hosts must merge // rather than delete aliases when completeness is unknown. bool title_aliases_complete = 36; + // ISO 639-1 language of overview and tagline. Empty means the text is in the + // requested language or the plugin cannot tell; set it when the provider + // had no text in the requested language and returned another language's. + string overview_language = 37; } message GetMetadataRequest { @@ -185,6 +189,10 @@ message SeasonRecord { google.protobuf.Struct metadata = 6; string provider_id = 7; google.protobuf.Struct provider_ids = 8; + // ISO 639-1 languages of title and overview, with MetadataItem's + // overview_language semantics: empty means the requested language or unknown. + string title_language = 9; + string overview_language = 10; } message GetSeasonsRequest { @@ -209,6 +217,10 @@ message EpisodeRecord { google.protobuf.Struct ratings = 9; google.protobuf.Struct metadata = 10; string provider_id = 11; + // ISO 639-1 languages of title and overview, with MetadataItem's + // overview_language semantics: empty means the requested language or unknown. + string title_language = 12; + string overview_language = 13; } message GetEpisodesRequest { From 4598c97aa01b667bab477d40610883cfadee3a36 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:16:00 -0400 Subject: [PATCH 2/3] feat(markers): add the file hash, marker source and absent segments to the marker contract (#50) * feat(markers): add file facts and absent segments to the marker contract FetchMarkersRequest carries the file's OpenSubtitles hash and size and whether the host understands absent segments. MarkerSegment gains absent. SubmitMarkerRequest carries the file facts, the marker's source, algorithm and confidence, and an absent flag. Co-Authored-By: Claude Opus 5.5 * docs(markers): document the marker provider contract and its metadata keys Adds docs/marker-provider.md with the capability metadata keys, the v0.29.0 file facts and absent segments, and the rule that a plugin must not send the raw OpenSubtitles hash off the machine. Links it from the README and notes the compatibility rules for older hosts. Co-Authored-By: Claude Opus 5.5 * docs(markers): correct the file hash guidance and metadata key versioning An unsalted SHA-256 of the OpenSubtitles hash is not service-specific, so say what it hides and when a salt or HMAC is needed. The capability metadata keys are read by the host, not the SDK, so drop the SDK version labels from them. Co-Authored-By: Claude Opus 5.5 * docs(markers): describe file_hash as a fingerprint that files can share The OpenSubtitles hash covers only the size and the first and last 64 KiB, so it does not prove two files hold the same bytes. Co-Authored-By: Claude Opus 5.5 * test(markers): cover old payloads and absent segment bounds on decode Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- README.md | 8 ++ docs/compatibility.md | 8 ++ docs/marker-provider.md | 99 +++++++++++++ .../silo/plugin/v1/marker_provider.pb.go | 133 ++++++++++++++++-- .../silo/plugin/v1/marker_provider_test.go | 55 ++++++++ proto/silo/plugin/v1/marker_provider.proto | 28 ++++ 6 files changed, 320 insertions(+), 11 deletions(-) create mode 100644 docs/marker-provider.md diff --git a/README.md b/README.md index 9d42773..657be4f 100644 --- a/README.md +++ b/README.md @@ -443,6 +443,14 @@ while omission means no requested ordering. A page may carry `warnings`, safe notes such as items skipped for missing identity, which the host shows with the sync run. +## Marker providers + +`marker_provider.v1` fetches intro, credits, recap, and preview markers from a +marker service and can submit the host's markers to it. Settings such as the +starting fetch priority and contribution defaults travel in the capability's +`metadata`. See [docs/marker-provider.md](docs/marker-provider.md) for those +keys, the file facts the host sends, and absent segments. + ## Network access providers `network_access_provider.v1` lets a resident plugin give the deployment an diff --git a/docs/compatibility.md b/docs/compatibility.md index 166417c..e219f70 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -190,6 +190,14 @@ overview. Hosts rank that text below another provider's text in the requested language and may replace it with text fetched in a fallback language. Hosts that predate the fields ignore them. +The marker fields added in v0.30.0 are plain scalars, so they have no +presence. A host that predates them sends an empty `file_hash`, a `file_size` +of 0, `accepts_absent` false, and submissions without `source`, `algorithm`, +`confidence`, or `absent`. A marker provider must not return a +`MarkerSegment` with `absent` set unless the request's `accepts_absent` is +true: an older host reads a segment without bounds as covering the whole +file. See [marker-provider.md](marker-provider.md). + 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/docs/marker-provider.md b/docs/marker-provider.md new file mode 100644 index 0000000..2f835cb --- /dev/null +++ b/docs/marker-provider.md @@ -0,0 +1,99 @@ +# Marker providers (`marker_provider.v1`) + +A marker provider supplies intro, credits, recap, and preview markers for +movies and episodes, and can accept markers the host submits. The host owns +local detection, provider order, the contribution settings, and when to fetch +or submit. The plugin talks to its marker service. + +## Manifest + +Declare the capability with free-form `metadata`. The host reads the keys +below from it; there is no typed descriptor. + +```json +{ + "type": "marker_provider.v1", + "id": "introdb", + "display_name": "TheIntroDB", + "metadata": { + "required_external_ids": ["tmdb"], + "default_fetch_priority": 100 + } +} +``` + +| Key | Type | Default | Meaning | +|---|---|---|---| +| `default_fetch_priority` | number | next free | Fetch priority the provider starts with when first configured. The host asks providers in ascending order. | +| `supports_contribution` | bool | true | `false` makes the provider fetch-only, and the host never calls `SubmitMarker`. | +| `required_external_ids` | list of strings | none | External ID keys (`tmdb`, `tvdb`, `imdb`) the title must have before the host submits to the provider. All of them are required. | +| `default_contribute_enabled` | bool | false | Contribution starts on when the provider is first configured. | +| `default_contribute_auto_local` | bool | false | Automatic sharing of local detections starts on. | +| `default_contribute_min_confidence` | number, 0–1 | 0.95 | Starting minimum confidence for automatic sharing. | +| `supports_amend` | bool | false | The provider replaces a server's earlier submission for the same file and kind, so the host may resubmit when the marker changes. | +| `supports_absent` | bool | false | The provider accepts `absent` submissions. | + +The defaults apply only when the host first creates the provider's +configuration, and an administrator's later changes win. A host may ignore +`default_contribute_enabled` and `default_contribute_auto_local` when its +administrator has chosen not to share by default. Hosts that predate a key +ignore it, so declaring one does not raise the plugin's minimum host version. +The SDK does not read these keys, so declaring one needs no particular SDK +version. Only hosts that implement a key honour it. + +## Fetching + +`FetchMarkersRequest` names the item by its title IDs (`external_ids`) and, +for an episode, by season and episode number, `episode_external_ids`, +`episode_title`, and `episode_numbering`. The comments in +[`marker_provider.proto`](../proto/silo/plugin/v1/marker_provider.proto) give +the exact rules. + +Since v0.30.0 the request also carries: + +- `file_hash`: the file's OpenSubtitles hash (16 lowercase hex characters), + or empty when the host has none. The hash covers the file size and the + first and last 64 KiB, so a remux or re-encode gets a new hash. It is a + fingerprint, not proof that two files hold the same bytes: different files + can share one. +- `file_size`: the file size in bytes, or 0 when unknown. +- `accepts_absent`: the host understands `MarkerSegment.absent`. + +`file_hash` is the raw OpenSubtitles hash, and it identifies a file across +every server that holds the same bytes. A plugin must not send it off the +machine unhashed. Hash it before sending, for example +`hex(sha256(file_hash))[0:16]` as Silo's marker service does. An unsalted +hash like this hides the raw value, but anyone who holds the OpenSubtitles +hash can compute it, and every service that uses the same scheme gets the +same identifier. Use a service-specific salt or HMAC key when the service +must not be linkable to others. + +A `MarkerSegment` with `absent` set asserts that the item has no segment of +that kind. Its `start_seconds` and `end_seconds` are unset, and `confidence` +applies to the absence. A provider must not return absent segments unless +`accepts_absent` is true: a host older than the field reads a segment without +bounds as covering the whole file. + +## Submitting + +`SubmitMarkerRequest` names the item the same way as `FetchMarkersRequest` +and carries one segment with its bounds. Since v0.30.0 it also carries: + +- `file_hash` and `file_size`, as in `FetchMarkersRequest`, with the same + rule about `file_hash`. +- `source`: `scanner` for a local detection, including chapters, or `manual` + for a marker a person edited. Empty from older hosts. +- `algorithm`: the host's algorithm ID for the marker, such as + `chromaprint:dialogue:v4`, or `manual:v1`. +- `confidence`: the host's confidence in the marker, 0–1. +- `absent`: the submission says the item has no segment of this kind, and + the bounds are unset. Hosts send it only to providers that declare + `supports_absent`. + +## Compatibility + +All the v0.30.0 fields are additive. A host older than v0.30.0 leaves them +empty: no file facts, `accepts_absent` false, and no `source`, `algorithm`, +`confidence`, or `absent` on submissions. Providers must handle an empty +`file_hash` and a `file_size` of 0, and treat a submission with an empty +`source` as one whose origin is unknown. diff --git a/pkg/pluginproto/silo/plugin/v1/marker_provider.pb.go b/pkg/pluginproto/silo/plugin/v1/marker_provider.pb.go index 5131683..cab3ceb 100644 --- a/pkg/pluginproto/silo/plugin/v1/marker_provider.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/marker_provider.pb.go @@ -112,8 +112,19 @@ type FetchMarkersRequest struct { EpisodeExternalIds *MarkerExternalIDs `protobuf:"bytes,7,opt,name=episode_external_ids,json=episodeExternalIds,proto3" json:"episode_external_ids,omitempty"` EpisodeTitle string `protobuf:"bytes,8,opt,name=episode_title,json=episodeTitle,proto3" json:"episode_title,omitempty"` EpisodeNumbering string `protobuf:"bytes,9,opt,name=episode_numbering,json=episodeNumbering,proto3" json:"episode_numbering,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // OpenSubtitles hash of the file (16 lowercase hex), or empty when the host + // has none. It covers the file size and the first and last 64 KiB, so a + // remux or re-encode gets a new hash, but two different files can share + // one. A provider must not send it off the machine unhashed. + FileHash string `protobuf:"bytes,10,opt,name=file_hash,json=fileHash,proto3" json:"file_hash,omitempty"` + // File size in bytes, or 0 when unknown. + FileSize int64 `protobuf:"varint,11,opt,name=file_size,json=fileSize,proto3" json:"file_size,omitempty"` + // The host understands MarkerSegment.absent. A provider must not return + // absent segments when this is false: older hosts read a segment without + // bounds as covering the whole file. + AcceptsAbsent bool `protobuf:"varint,12,opt,name=accepts_absent,json=acceptsAbsent,proto3" json:"accepts_absent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FetchMarkersRequest) Reset() { @@ -209,6 +220,27 @@ func (x *FetchMarkersRequest) GetEpisodeNumbering() string { return "" } +func (x *FetchMarkersRequest) GetFileHash() string { + if x != nil { + return x.FileHash + } + return "" +} + +func (x *FetchMarkersRequest) GetFileSize() int64 { + if x != nil { + return x.FileSize + } + return 0 +} + +func (x *FetchMarkersRequest) GetAcceptsAbsent() bool { + if x != nil { + return x.AcceptsAbsent + } + return false +} + type MarkerSegment struct { state protoimpl.MessageState `protogen:"open.v1"` Segment string `protobuf:"bytes,1,opt,name=segment,proto3" json:"segment,omitempty"` // "intro" | "credits" | "recap" | "preview" @@ -218,8 +250,11 @@ type MarkerSegment struct { SubmissionCount int32 `protobuf:"varint,5,opt,name=submission_count,json=submissionCount,proto3" json:"submission_count,omitempty"` Algorithm string `protobuf:"bytes,6,opt,name=algorithm,proto3" json:"algorithm,omitempty"` Metadata *structpb.Struct `protobuf:"bytes,7,opt,name=metadata,proto3" json:"metadata,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The provider asserts the item has no segment of this kind. start_seconds + // and end_seconds are unset; confidence applies to the absence. + Absent bool `protobuf:"varint,8,opt,name=absent,proto3" json:"absent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *MarkerSegment) Reset() { @@ -301,6 +336,13 @@ func (x *MarkerSegment) GetMetadata() *structpb.Struct { return nil } +func (x *MarkerSegment) GetAbsent() bool { + if x != nil { + return x.Absent + } + return false +} + type FetchMarkersResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Markers []*MarkerSegment `protobuf:"bytes,1,rep,name=markers,proto3" json:"markers,omitempty"` @@ -368,8 +410,22 @@ type SubmitMarkerRequest struct { EpisodeExternalIds *MarkerExternalIDs `protobuf:"bytes,10,opt,name=episode_external_ids,json=episodeExternalIds,proto3" json:"episode_external_ids,omitempty"` EpisodeTitle string `protobuf:"bytes,11,opt,name=episode_title,json=episodeTitle,proto3" json:"episode_title,omitempty"` EpisodeNumbering string `protobuf:"bytes,12,opt,name=episode_numbering,json=episodeNumbering,proto3" json:"episode_numbering,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + FileHash string `protobuf:"bytes,13,opt,name=file_hash,json=fileHash,proto3" json:"file_hash,omitempty"` // as FetchMarkersRequest.file_hash + FileSize int64 `protobuf:"varint,14,opt,name=file_size,json=fileSize,proto3" json:"file_size,omitempty"` + // Where the host's marker came from: "scanner" (local detection, including + // chapters) or "manual" (edited by a person). Empty from hosts older than + // this field. + Source string `protobuf:"bytes,15,opt,name=source,proto3" json:"source,omitempty"` + // The host's algorithm ID for the marker, e.g. "chromaprint:dialogue:v4", + // or "manual:v1". + Algorithm string `protobuf:"bytes,16,opt,name=algorithm,proto3" json:"algorithm,omitempty"` + // The host's confidence in the marker, 0-1. + Confidence float64 `protobuf:"fixed64,17,opt,name=confidence,proto3" json:"confidence,omitempty"` + // The submission says the item has no segment of this kind. start_seconds + // and end_seconds are unset. + Absent bool `protobuf:"varint,18,opt,name=absent,proto3" json:"absent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SubmitMarkerRequest) Reset() { @@ -486,6 +542,48 @@ func (x *SubmitMarkerRequest) GetEpisodeNumbering() string { return "" } +func (x *SubmitMarkerRequest) GetFileHash() string { + if x != nil { + return x.FileHash + } + return "" +} + +func (x *SubmitMarkerRequest) GetFileSize() int64 { + if x != nil { + return x.FileSize + } + return 0 +} + +func (x *SubmitMarkerRequest) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *SubmitMarkerRequest) GetAlgorithm() string { + if x != nil { + return x.Algorithm + } + return "" +} + +func (x *SubmitMarkerRequest) GetConfidence() float64 { + if x != nil { + return x.Confidence + } + return 0 +} + +func (x *SubmitMarkerRequest) GetAbsent() bool { + if x != nil { + return x.Absent + } + return false +} + type SubmitMarkerResponse struct { state protoimpl.MessageState `protogen:"open.v1"` SubmissionId string `protobuf:"bytes,1,opt,name=submission_id,json=submissionId,proto3" json:"submission_id,omitempty"` @@ -699,7 +797,7 @@ const file_silo_plugin_v1_marker_provider_proto_rawDesc = "" + "\atmdb_id\x18\x01 \x01(\tR\x06tmdbId\x12\x17\n" + "\aimdb_id\x18\x02 \x01(\tR\x06imdbId\x12\x17\n" + "\atvdb_id\x18\x03 \x01(\tR\x06tvdbId\x12:\n" + - "\fprovider_ids\x18\x04 \x01(\v2\x17.google.protobuf.StructR\vproviderIds\"\xcb\x03\n" + + "\fprovider_ids\x18\x04 \x01(\v2\x17.google.protobuf.StructR\vproviderIds\"\xac\x04\n" + "\x13FetchMarkersRequest\x12\x1b\n" + "\titem_type\x18\x01 \x01(\tR\bitemType\x12D\n" + "\fexternal_ids\x18\x02 \x01(\v2!.silo.plugin.v1.MarkerExternalIDsR\vexternalIds\x12#\n" + @@ -709,7 +807,11 @@ const file_silo_plugin_v1_marker_provider_proto_rawDesc = "" + "\bmetadata\x18\x06 \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12S\n" + "\x14episode_external_ids\x18\a \x01(\v2!.silo.plugin.v1.MarkerExternalIDsR\x12episodeExternalIds\x12#\n" + "\repisode_title\x18\b \x01(\tR\fepisodeTitle\x12+\n" + - "\x11episode_numbering\x18\t \x01(\tR\x10episodeNumbering\"\xb9\x02\n" + + "\x11episode_numbering\x18\t \x01(\tR\x10episodeNumbering\x12\x1b\n" + + "\tfile_hash\x18\n" + + " \x01(\tR\bfileHash\x12\x1b\n" + + "\tfile_size\x18\v \x01(\x03R\bfileSize\x12%\n" + + "\x0eaccepts_absent\x18\f \x01(\bR\racceptsAbsent\"\xd1\x02\n" + "\rMarkerSegment\x12\x18\n" + "\asegment\x18\x01 \x01(\tR\asegment\x12(\n" + "\rstart_seconds\x18\x02 \x01(\x01H\x00R\fstartSeconds\x88\x01\x01\x12$\n" + @@ -720,12 +822,13 @@ const file_silo_plugin_v1_marker_provider_proto_rawDesc = "" + "confidence\x12)\n" + "\x10submission_count\x18\x05 \x01(\x05R\x0fsubmissionCount\x12\x1c\n" + "\talgorithm\x18\x06 \x01(\tR\talgorithm\x123\n" + - "\bmetadata\x18\a \x01(\v2\x17.google.protobuf.StructR\bmetadataB\x10\n" + + "\bmetadata\x18\a \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12\x16\n" + + "\x06absent\x18\b \x01(\bR\x06absentB\x10\n" + "\x0e_start_secondsB\x0e\n" + "\f_end_seconds\"\x84\x01\n" + "\x14FetchMarkersResponse\x127\n" + "\amarkers\x18\x01 \x03(\v2\x1d.silo.plugin.v1.MarkerSegmentR\amarkers\x123\n" + - "\bmetadata\x18\x02 \x01(\v2\x17.google.protobuf.StructR\bmetadata\"\xd7\x04\n" + + "\bmetadata\x18\x02 \x01(\v2\x17.google.protobuf.StructR\bmetadata\"\xff\x05\n" + "\x13SubmitMarkerRequest\x12\x1b\n" + "\titem_type\x18\x01 \x01(\tR\bitemType\x12D\n" + "\fexternal_ids\x18\x02 \x01(\v2!.silo.plugin.v1.MarkerExternalIDsR\vexternalIds\x12#\n" + @@ -740,7 +843,15 @@ const file_silo_plugin_v1_marker_provider_proto_rawDesc = "" + "\x14episode_external_ids\x18\n" + " \x01(\v2!.silo.plugin.v1.MarkerExternalIDsR\x12episodeExternalIds\x12#\n" + "\repisode_title\x18\v \x01(\tR\fepisodeTitle\x12+\n" + - "\x11episode_numbering\x18\f \x01(\tR\x10episodeNumberingB\x10\n" + + "\x11episode_numbering\x18\f \x01(\tR\x10episodeNumbering\x12\x1b\n" + + "\tfile_hash\x18\r \x01(\tR\bfileHash\x12\x1b\n" + + "\tfile_size\x18\x0e \x01(\x03R\bfileSize\x12\x16\n" + + "\x06source\x18\x0f \x01(\tR\x06source\x12\x1c\n" + + "\talgorithm\x18\x10 \x01(\tR\talgorithm\x12\x1e\n" + + "\n" + + "confidence\x18\x11 \x01(\x01R\n" + + "confidence\x12\x16\n" + + "\x06absent\x18\x12 \x01(\bR\x06absentB\x10\n" + "\x0e_start_secondsB\x0e\n" + "\f_end_seconds\"\xa0\x01\n" + "\x14SubmitMarkerResponse\x12#\n" + diff --git a/pkg/pluginproto/silo/plugin/v1/marker_provider_test.go b/pkg/pluginproto/silo/plugin/v1/marker_provider_test.go index f61962b..e3e6e75 100644 --- a/pkg/pluginproto/silo/plugin/v1/marker_provider_test.go +++ b/pkg/pluginproto/silo/plugin/v1/marker_provider_test.go @@ -15,6 +15,19 @@ func TestMarkerRequests_EpisodeIdentityFieldNumbers(t *testing.T) { assertFieldNumber(t, &SubmitMarkerRequest{}, "episode_numbering", 12) } +func TestMarkerMessages_FileFactsAndAbsentFieldNumbers(t *testing.T) { + assertFieldNumber(t, &FetchMarkersRequest{}, "file_hash", 10) + assertFieldNumber(t, &FetchMarkersRequest{}, "file_size", 11) + assertFieldNumber(t, &FetchMarkersRequest{}, "accepts_absent", 12) + assertFieldNumber(t, &MarkerSegment{}, "absent", 8) + assertFieldNumber(t, &SubmitMarkerRequest{}, "file_hash", 13) + assertFieldNumber(t, &SubmitMarkerRequest{}, "file_size", 14) + assertFieldNumber(t, &SubmitMarkerRequest{}, "source", 15) + assertFieldNumber(t, &SubmitMarkerRequest{}, "algorithm", 16) + assertFieldNumber(t, &SubmitMarkerRequest{}, "confidence", 17) + assertFieldNumber(t, &SubmitMarkerRequest{}, "absent", 18) +} + // A host that predates the episode fields sends requests without them; the // plugin must see the series identity unchanged and empty episode fields. func TestFetchMarkersRequest_OldPayloadDecodesWithoutEpisodeIdentity(t *testing.T) { @@ -39,6 +52,48 @@ func TestFetchMarkersRequest_OldPayloadDecodesWithoutEpisodeIdentity(t *testing. } } +// A host that predates the file facts sends requests without them, and an +// absent segment reaches any host with no bounds, which an older host reads +// as covering the whole file. +func TestMarkerMessages_OldPayloadsAndAbsentSegmentPresence(t *testing.T) { + fetchBytes, err := proto.Marshal(&FetchMarkersRequest{ItemType: "movie", ExternalIds: &MarkerExternalIDs{TmdbId: "603"}}) + if err != nil { + t.Fatalf("marshal fetch: %v", err) + } + var fetch FetchMarkersRequest + if err := proto.Unmarshal(fetchBytes, &fetch); err != nil { + t.Fatalf("unmarshal fetch: %v", err) + } + if fetch.GetFileHash() != "" || fetch.GetFileSize() != 0 || fetch.GetAcceptsAbsent() { + t.Fatalf("fetch file facts = %q/%d/%t, want empty", fetch.GetFileHash(), fetch.GetFileSize(), fetch.GetAcceptsAbsent()) + } + + submitBytes, err := proto.Marshal(&SubmitMarkerRequest{ItemType: "movie", Segment: "intro", StartSeconds: proto.Float64(0), EndSeconds: proto.Float64(30)}) + if err != nil { + t.Fatalf("marshal submit: %v", err) + } + var submit SubmitMarkerRequest + if err := proto.Unmarshal(submitBytes, &submit); err != nil { + t.Fatalf("unmarshal submit: %v", err) + } + if submit.GetFileHash() != "" || submit.GetFileSize() != 0 || submit.GetSource() != "" || + submit.GetAlgorithm() != "" || submit.GetConfidence() != 0 || submit.GetAbsent() { + t.Fatalf("submit new fields not empty: %+v", &submit) + } + + segBytes, err := proto.Marshal(&MarkerSegment{Segment: "intro", Absent: true, Confidence: 0.8}) + if err != nil { + t.Fatalf("marshal segment: %v", err) + } + var seg MarkerSegment + if err := proto.Unmarshal(segBytes, &seg); err != nil { + t.Fatalf("unmarshal segment: %v", err) + } + if !seg.GetAbsent() || seg.StartSeconds != nil || seg.EndSeconds != nil { + t.Fatalf("absent segment = absent %t start %v end %v, want absent with no bounds", seg.GetAbsent(), seg.StartSeconds, seg.EndSeconds) + } +} + func TestSubmitMarkerRequest_EpisodeIdentityRoundTrips(t *testing.T) { data, err := proto.Marshal(&SubmitMarkerRequest{ ItemType: "episode", diff --git a/proto/silo/plugin/v1/marker_provider.proto b/proto/silo/plugin/v1/marker_provider.proto index 63cb02d..1e05ce2 100644 --- a/proto/silo/plugin/v1/marker_provider.proto +++ b/proto/silo/plugin/v1/marker_provider.proto @@ -40,6 +40,17 @@ message FetchMarkersRequest { MarkerExternalIDs episode_external_ids = 7; string episode_title = 8; string episode_numbering = 9; + // OpenSubtitles hash of the file (16 lowercase hex), or empty when the host + // has none. It covers the file size and the first and last 64 KiB, so a + // remux or re-encode gets a new hash, but two different files can share + // one. A provider must not send it off the machine unhashed. + string file_hash = 10; + // File size in bytes, or 0 when unknown. + int64 file_size = 11; + // The host understands MarkerSegment.absent. A provider must not return + // absent segments when this is false: older hosts read a segment without + // bounds as covering the whole file. + bool accepts_absent = 12; } message MarkerSegment { @@ -50,6 +61,9 @@ message MarkerSegment { int32 submission_count = 5; string algorithm = 6; google.protobuf.Struct metadata = 7; + // The provider asserts the item has no segment of this kind. start_seconds + // and end_seconds are unset; confidence applies to the absence. + bool absent = 8; } message FetchMarkersResponse { @@ -71,6 +85,20 @@ message SubmitMarkerRequest { MarkerExternalIDs episode_external_ids = 10; string episode_title = 11; string episode_numbering = 12; + string file_hash = 13; // as FetchMarkersRequest.file_hash + int64 file_size = 14; + // Where the host's marker came from: "scanner" (local detection, including + // chapters) or "manual" (edited by a person). Empty from hosts older than + // this field. + string source = 15; + // The host's algorithm ID for the marker, e.g. "chromaprint:dialogue:v4", + // or "manual:v1". + string algorithm = 16; + // The host's confidence in the marker, 0-1. + double confidence = 17; + // The submission says the item has no segment of this kind. start_seconds + // and end_seconds are unset. + bool absent = 18; } message SubmitMarkerResponse { From 6878e5f83774ce336e1e0f70c762755206a36652 Mon Sep 17 00:00:00 2001 From: "prairie-sync[bot]" Date: Sat, 24 Oct 2026 13:22:54 +0000 Subject: [PATCH 3/3] Regenerate generated files after the upstream sync Ran: command -v protoc >/dev/null || (sudo apt-get update -qq && sudo apt-get install -y -qq protobuf-compiler); make proto; go mod tidy; --- .../prairie/plugin/v1/marker_provider.pb.go | 38 ++++---- .../prairie/plugin/v1/metadata_provider.pb.go | 88 +++++++++---------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/pkg/pluginproto/prairie/plugin/v1/marker_provider.pb.go b/pkg/pluginproto/prairie/plugin/v1/marker_provider.pb.go index 50d55f1..585dc38 100644 --- a/pkg/pluginproto/prairie/plugin/v1/marker_provider.pb.go +++ b/pkg/pluginproto/prairie/plugin/v1/marker_provider.pb.go @@ -792,20 +792,20 @@ var File_prairie_plugin_v1_marker_provider_proto protoreflect.FileDescriptor const file_prairie_plugin_v1_marker_provider_proto_rawDesc = "" + "\n" + - "$prairie/plugin/v1/marker_provider.proto\x12\x0eprairie.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\"\x9a\x01\n" + + "'prairie/plugin/v1/marker_provider.proto\x12\x11prairie.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\"\x9a\x01\n" + "\x11MarkerExternalIDs\x12\x17\n" + "\atmdb_id\x18\x01 \x01(\tR\x06tmdbId\x12\x17\n" + "\aimdb_id\x18\x02 \x01(\tR\x06imdbId\x12\x17\n" + "\atvdb_id\x18\x03 \x01(\tR\x06tvdbId\x12:\n" + - "\fprovider_ids\x18\x04 \x01(\v2\x17.google.protobuf.StructR\vproviderIds\"\xac\x04\n" + + "\fprovider_ids\x18\x04 \x01(\v2\x17.google.protobuf.StructR\vproviderIds\"\xb2\x04\n" + "\x13FetchMarkersRequest\x12\x1b\n" + - "\titem_type\x18\x01 \x01(\tR\bitemType\x12D\n" + - "\fexternal_ids\x18\x02 \x01(\v2!.prairie.plugin.v1.MarkerExternalIDsR\vexternalIds\x12#\n" + + "\titem_type\x18\x01 \x01(\tR\bitemType\x12G\n" + + "\fexternal_ids\x18\x02 \x01(\v2$.prairie.plugin.v1.MarkerExternalIDsR\vexternalIds\x12#\n" + "\rseason_number\x18\x03 \x01(\x05R\fseasonNumber\x12%\n" + "\x0eepisode_number\x18\x04 \x01(\x05R\repisodeNumber\x12)\n" + "\x10duration_seconds\x18\x05 \x01(\x01R\x0fdurationSeconds\x123\n" + - "\bmetadata\x18\x06 \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12S\n" + - "\x14episode_external_ids\x18\a \x01(\v2!.prairie.plugin.v1.MarkerExternalIDsR\x12episodeExternalIds\x12#\n" + + "\bmetadata\x18\x06 \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12V\n" + + "\x14episode_external_ids\x18\a \x01(\v2$.prairie.plugin.v1.MarkerExternalIDsR\x12episodeExternalIds\x12#\n" + "\repisode_title\x18\b \x01(\tR\fepisodeTitle\x12+\n" + "\x11episode_numbering\x18\t \x01(\tR\x10episodeNumbering\x12\x1b\n" + "\tfile_hash\x18\n" + @@ -825,13 +825,13 @@ const file_prairie_plugin_v1_marker_provider_proto_rawDesc = "" + "\bmetadata\x18\a \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12\x16\n" + "\x06absent\x18\b \x01(\bR\x06absentB\x10\n" + "\x0e_start_secondsB\x0e\n" + - "\f_end_seconds\"\x84\x01\n" + - "\x14FetchMarkersResponse\x127\n" + - "\amarkers\x18\x01 \x03(\v2\x1d.prairie.plugin.v1.MarkerSegmentR\amarkers\x123\n" + - "\bmetadata\x18\x02 \x01(\v2\x17.google.protobuf.StructR\bmetadata\"\xff\x05\n" + + "\f_end_seconds\"\x87\x01\n" + + "\x14FetchMarkersResponse\x12:\n" + + "\amarkers\x18\x01 \x03(\v2 .prairie.plugin.v1.MarkerSegmentR\amarkers\x123\n" + + "\bmetadata\x18\x02 \x01(\v2\x17.google.protobuf.StructR\bmetadata\"\x85\x06\n" + "\x13SubmitMarkerRequest\x12\x1b\n" + - "\titem_type\x18\x01 \x01(\tR\bitemType\x12D\n" + - "\fexternal_ids\x18\x02 \x01(\v2!.prairie.plugin.v1.MarkerExternalIDsR\vexternalIds\x12#\n" + + "\titem_type\x18\x01 \x01(\tR\bitemType\x12G\n" + + "\fexternal_ids\x18\x02 \x01(\v2$.prairie.plugin.v1.MarkerExternalIDsR\vexternalIds\x12#\n" + "\rseason_number\x18\x03 \x01(\x05R\fseasonNumber\x12%\n" + "\x0eepisode_number\x18\x04 \x01(\x05R\repisodeNumber\x12\x18\n" + "\asegment\x18\x05 \x01(\tR\asegment\x12(\n" + @@ -839,9 +839,9 @@ const file_prairie_plugin_v1_marker_provider_proto_rawDesc = "" + "\vend_seconds\x18\a \x01(\x01H\x01R\n" + "endSeconds\x88\x01\x01\x12)\n" + "\x10duration_seconds\x18\b \x01(\x01R\x0fdurationSeconds\x123\n" + - "\bmetadata\x18\t \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12S\n" + + "\bmetadata\x18\t \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12V\n" + "\x14episode_external_ids\x18\n" + - " \x01(\v2!.prairie.plugin.v1.MarkerExternalIDsR\x12episodeExternalIds\x12#\n" + + " \x01(\v2$.prairie.plugin.v1.MarkerExternalIDsR\x12episodeExternalIds\x12#\n" + "\repisode_title\x18\v \x01(\tR\fepisodeTitle\x12+\n" + "\x11episode_numbering\x18\f \x01(\tR\x10episodeNumbering\x12\x1b\n" + "\tfile_hash\x18\r \x01(\tR\bfileHash\x12\x1b\n" + @@ -869,11 +869,11 @@ const file_prairie_plugin_v1_marker_provider_proto_rawDesc = "" + "\x0ecurrent_streak\x18\x06 \x01(\x05R\rcurrentStreak\x12\x1f\n" + "\vbest_streak\x18\a \x01(\x05R\n" + "bestStreak\x123\n" + - "\bmetadata\x18\b \x01(\v2\x17.google.protobuf.StructR\bmetadata2\xbc\x02\n" + - "\x0eMarkerProvider\x12Y\n" + - "\fFetchMarkers\x12#.prairie.plugin.v1.FetchMarkersRequest\x1a$.prairie.plugin.v1.FetchMarkersResponse\x12Y\n" + - "\fSubmitMarker\x12#.prairie.plugin.v1.SubmitMarkerRequest\x1a$.prairie.plugin.v1.SubmitMarkerResponse\x12t\n" + - "\x16GetMarkerProviderStats\x12-.prairie.plugin.v1.GetMarkerProviderStatsRequest\x1a+.prairie.plugin.v1.MarkerProviderStatsResponseBPZNgithub.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1;pluginv1b\x06proto3" + "\bmetadata\x18\b \x01(\v2\x17.google.protobuf.StructR\bmetadata2\xce\x02\n" + + "\x0eMarkerProvider\x12_\n" + + "\fFetchMarkers\x12&.prairie.plugin.v1.FetchMarkersRequest\x1a'.prairie.plugin.v1.FetchMarkersResponse\x12_\n" + + "\fSubmitMarker\x12&.prairie.plugin.v1.SubmitMarkerRequest\x1a'.prairie.plugin.v1.SubmitMarkerResponse\x12z\n" + + "\x16GetMarkerProviderStats\x120.prairie.plugin.v1.GetMarkerProviderStatsRequest\x1a..prairie.plugin.v1.MarkerProviderStatsResponseBYZWgithub.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1;pluginv1b\x06proto3" var ( file_prairie_plugin_v1_marker_provider_proto_rawDescOnce sync.Once diff --git a/pkg/pluginproto/prairie/plugin/v1/metadata_provider.pb.go b/pkg/pluginproto/prairie/plugin/v1/metadata_provider.pb.go index 1f67a6f..986fef6 100644 --- a/pkg/pluginproto/prairie/plugin/v1/metadata_provider.pb.go +++ b/pkg/pluginproto/prairie/plugin/v1/metadata_provider.pb.go @@ -2155,12 +2155,12 @@ var File_prairie_plugin_v1_metadata_provider_proto protoreflect.FileDescriptor const file_prairie_plugin_v1_metadata_provider_proto_rawDesc = "" + "\n" + - "&prairie/plugin/v1/metadata_provider.proto\x12\x0eprairie.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\"R\n" + + ")prairie/plugin/v1/metadata_provider.proto\x12\x11prairie.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\"R\n" + "\n" + "TitleAlias\x12\x14\n" + "\x05title\x18\x01 \x01(\tR\x05title\x12\x1a\n" + "\blanguage\x18\x02 \x01(\tR\blanguage\x12\x12\n" + - "\x04kind\x18\x03 \x01(\tR\x04kind\"\xdb\x03\n" + + "\x04kind\x18\x03 \x01(\tR\x04kind\"\xde\x03\n" + "\x14ProviderSearchResult\x12\x1f\n" + "\vprovider_id\x18\x01 \x01(\tR\n" + "providerId\x12\x1b\n" + @@ -2170,8 +2170,8 @@ const file_prairie_plugin_v1_metadata_provider_proto_rawDesc = "" + "\x04year\x18\x05 \x01(\x05R\x04year\x12\x1a\n" + "\boverview\x18\x06 \x01(\tR\boverview\x12:\n" + "\fprovider_ids\x18\a \x01(\v2\x17.google.protobuf.StructR\vproviderIds\x12\x1b\n" + - "\timage_url\x18\b \x01(\tR\bimageUrl\x12?\n" + - "\rtitle_aliases\x18\t \x03(\v2\x1a.prairie.plugin.v1.TitleAliasR\ftitleAliases\x12%\n" + + "\timage_url\x18\b \x01(\tR\bimageUrl\x12B\n" + + "\rtitle_aliases\x18\t \x03(\v2\x1d.prairie.plugin.v1.TitleAliasR\ftitleAliases\x12%\n" + "\x0etitle_language\x18\n" + " \x01(\tR\rtitleLanguage\x12*\n" + "\x11title_is_fallback\x18\v \x01(\bR\x0ftitleIsFallback\x12+\n" + @@ -2181,9 +2181,9 @@ const file_prairie_plugin_v1_metadata_provider_proto_rawDesc = "" + "\titem_type\x18\x02 \x01(\tR\bitemType\x12\x12\n" + "\x04year\x18\x03 \x01(\x05R\x04year\x12:\n" + "\fprovider_ids\x18\x04 \x01(\v2\x17.google.protobuf.StructR\vproviderIds\x12\x1a\n" + - "\blanguage\x18\x05 \x01(\tR\blanguage\"X\n" + - "\x16SearchMetadataResponse\x12>\n" + - "\aresults\x18\x01 \x03(\v2$.prairie.plugin.v1.ProviderSearchResultR\aresults\"\xa3\x02\n" + + "\blanguage\x18\x05 \x01(\tR\blanguage\"[\n" + + "\x16SearchMetadataResponse\x12A\n" + + "\aresults\x18\x01 \x03(\v2'.prairie.plugin.v1.ProviderSearchResultR\aresults\"\xa3\x02\n" + "\fPersonRecord\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + "\x04kind\x18\x02 \x01(\tR\x04kind\x12\x1c\n" + @@ -2208,7 +2208,7 @@ const file_prairie_plugin_v1_metadata_provider_proto_rawDesc = "" + "\vis_official\x18\a \x01(\bR\n" + "isOfficial\x12\x1b\n" + "\tsize_hint\x18\b \x01(\x05R\bsizeHint\x12!\n" + - "\fpublished_at\x18\t \x01(\tR\vpublishedAt\"\x94\v\n" + + "\fpublished_at\x18\t \x01(\tR\vpublishedAt\"\x9d\v\n" + "\fMetadataItem\x12\x1f\n" + "\vprovider_id\x18\x01 \x01(\tR\n" + "providerId\x12\x1b\n" + @@ -2240,13 +2240,13 @@ const file_prairie_plugin_v1_metadata_provider_proto_rawDesc = "" + "\tlogo_path\x18\x18 \x01(\tR\blogoPath\x12!\n" + "\fseason_count\x18\x19 \x01(\x05R\vseasonCount\x12$\n" + "\x0efirst_air_date\x18\x1a \x01(\tR\ffirstAirDate\x12\"\n" + - "\rlast_air_date\x18\x1b \x01(\tR\vlastAirDate\x124\n" + - "\x06people\x18\x1c \x03(\v2\x1c.prairie.plugin.v1.PersonRecordR\x06people\x12!\n" + + "\rlast_air_date\x18\x1b \x01(\tR\vlastAirDate\x127\n" + + "\x06people\x18\x1c \x03(\v2\x1f.prairie.plugin.v1.PersonRecordR\x06people\x12!\n" + "\frelease_date\x18\x1d \x01(\tR\vreleaseDate\x12\x19\n" + "\bair_time\x18\x1e \x01(\tR\aairTime\x12\x16\n" + - "\x06status\x18\x1f \x01(\tR\x06status\x123\n" + - "\x06videos\x18 \x03(\v2\x1b.prairie.plugin.v1.VideoRecordR\x06videos\x12?\n" + - "\rtitle_aliases\x18! \x03(\v2\x1a.prairie.plugin.v1.TitleAliasR\ftitleAliases\x12%\n" + + "\x06status\x18\x1f \x01(\tR\x06status\x126\n" + + "\x06videos\x18 \x03(\v2\x1e.prairie.plugin.v1.VideoRecordR\x06videos\x12B\n" + + "\rtitle_aliases\x18! \x03(\v2\x1d.prairie.plugin.v1.TitleAliasR\ftitleAliases\x12%\n" + "\x0etitle_language\x18\" \x01(\tR\rtitleLanguage\x12*\n" + "\x11title_is_fallback\x18# \x01(\bR\x0ftitleIsFallback\x124\n" + "\x16title_aliases_complete\x18$ \x01(\bR\x14titleAliasesComplete\x12+\n" + @@ -2257,9 +2257,9 @@ const file_prairie_plugin_v1_metadata_provider_proto_rawDesc = "" + "\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\x12\x1b\n" + - "\tfile_path\x18\x05 \x01(\tR\bfilePath\"G\n" + - "\x13GetMetadataResponse\x120\n" + - "\x04item\x18\x01 \x01(\v2\x1c.prairie.plugin.v1.MetadataItemR\x04item\"p\n" + + "\tfile_path\x18\x05 \x01(\tR\bfilePath\"J\n" + + "\x13GetMetadataResponse\x123\n" + + "\x04item\x18\x01 \x01(\v2\x1f.prairie.plugin.v1.MetadataItemR\x04item\"p\n" + "\x16GetPersonDetailRequest\x12:\n" + "\fprovider_ids\x18\x01 \x01(\v2\x17.google.protobuf.StructR\vproviderIds\x12\x1a\n" + "\blanguage\x18\x02 \x01(\tR\blanguage\"\xd5\x02\n" + @@ -2279,9 +2279,9 @@ const file_prairie_plugin_v1_metadata_provider_proto_rawDesc = "" + "photo_path\x18\b \x01(\tR\tphotoPath\x12'\n" + "\x0fphoto_thumbhash\x18\t \x01(\tR\x0ephotoThumbhash\x12:\n" + "\fprovider_ids\x18\n" + - " \x01(\v2\x17.google.protobuf.StructR\vproviderIds\"U\n" + - "\x17GetPersonDetailResponse\x12:\n" + - "\x06person\x18\x01 \x01(\v2\".prairie.plugin.v1.PersonDetailRecordR\x06person\"\x87\x03\n" + + " \x01(\v2\x17.google.protobuf.StructR\vproviderIds\"X\n" + + "\x17GetPersonDetailResponse\x12=\n" + + "\x06person\x18\x01 \x01(\v2%.prairie.plugin.v1.PersonDetailRecordR\x06person\"\x87\x03\n" + "\fSeasonRecord\x12#\n" + "\rseason_number\x18\x01 \x01(\x05R\fseasonNumber\x12\x14\n" + "\x05title\x18\x02 \x01(\tR\x05title\x12\x1a\n" + @@ -2299,9 +2299,9 @@ const file_prairie_plugin_v1_metadata_provider_proto_rawDesc = "" + "\x11GetSeasonsRequest\x12,\n" + "\x12series_provider_id\x18\x01 \x01(\tR\x10seriesProviderId\x12:\n" + "\fprovider_ids\x18\x02 \x01(\v2\x17.google.protobuf.StructR\vproviderIds\x12\x1a\n" + - "\blanguage\x18\x03 \x01(\tR\blanguage\"L\n" + - "\x12GetSeasonsResponse\x126\n" + - "\aseasons\x18\x01 \x03(\v2\x1c.prairie.plugin.v1.SeasonRecordR\aseasons\"\xfa\x03\n" + + "\blanguage\x18\x03 \x01(\tR\blanguage\"O\n" + + "\x12GetSeasonsResponse\x129\n" + + "\aseasons\x18\x01 \x03(\v2\x1f.prairie.plugin.v1.SeasonRecordR\aseasons\"\xfa\x03\n" + "\rEpisodeRecord\x12#\n" + "\rseason_number\x18\x01 \x01(\x05R\fseasonNumber\x12%\n" + "\x0eepisode_number\x18\x02 \x01(\x05R\repisodeNumber\x12\x14\n" + @@ -2323,9 +2323,9 @@ const file_prairie_plugin_v1_metadata_provider_proto_rawDesc = "" + "\x12series_provider_id\x18\x01 \x01(\tR\x10seriesProviderId\x12#\n" + "\rseason_number\x18\x02 \x01(\x05R\fseasonNumber\x12:\n" + "\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.prairie.plugin.v1.EpisodeRecordR\bepisodes\"\xee\x01\n" + + "\blanguage\x18\x04 \x01(\tR\blanguage\"S\n" + + "\x13GetEpisodesResponse\x12<\n" + + "\bepisodes\x18\x01 \x03(\v2 .prairie.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" + @@ -2342,9 +2342,9 @@ const file_prairie_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\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.prairie.plugin.v1.ImageRecordR\x06images\"F\n" + + "\x0e_season_number\"K\n" + + "\x11GetImagesResponse\x126\n" + + "\x06images\x18\x01 \x03(\v2\x1e.prairie.plugin.v1.ImageRecordR\x06images\"F\n" + "\x16ResolveImageURLRequest\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x18\n" + "\avariant\x18\x02 \x01(\tR\avariant\"+\n" + @@ -2352,25 +2352,25 @@ const file_prairie_plugin_v1_metadata_provider_proto_rawDesc = "" + "\x03url\x18\x01 \x01(\tR\x03url\"I\n" + "\x17ResolveImageURLsRequest\x12\x14\n" + "\x05paths\x18\x01 \x03(\tR\x05paths\x12\x18\n" + - "\avariant\x18\x02 \x01(\tR\avariant\"\x9b\x01\n" + - "\x18ResolveImageURLsResponse\x12F\n" + - "\x04urls\x18\x01 \x03(\v22.prairie.plugin.v1.ResolveImageURLsResponse.UrlsEntryR\x04urls\x1a7\n" + + "\avariant\x18\x02 \x01(\tR\avariant\"\x9e\x01\n" + + "\x18ResolveImageURLsResponse\x12I\n" + + "\x04urls\x18\x01 \x03(\v25.prairie.plugin.v1.ResolveImageURLsResponse.UrlsEntryR\x04urls\x1a7\n" + "\tUrlsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x012\xf1\x05\n" + - "\x10MetadataProvider\x12W\n" + - "\x06Search\x12%.prairie.plugin.v1.SearchMetadataRequest\x1a&.prairie.plugin.v1.SearchMetadataResponse\x12V\n" + - "\vGetMetadata\x12\".prairie.plugin.v1.GetMetadataRequest\x1a#.prairie.plugin.v1.GetMetadataResponse\x12b\n" + - "\x0fGetPersonDetail\x12&.prairie.plugin.v1.GetPersonDetailRequest\x1a'.prairie.plugin.v1.GetPersonDetailResponse\x12S\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x012\xa1\x06\n" + + "\x10MetadataProvider\x12]\n" + + "\x06Search\x12(.prairie.plugin.v1.SearchMetadataRequest\x1a).prairie.plugin.v1.SearchMetadataResponse\x12\\\n" + + "\vGetMetadata\x12%.prairie.plugin.v1.GetMetadataRequest\x1a&.prairie.plugin.v1.GetMetadataResponse\x12h\n" + + "\x0fGetPersonDetail\x12).prairie.plugin.v1.GetPersonDetailRequest\x1a*.prairie.plugin.v1.GetPersonDetailResponse\x12Y\n" + "\n" + - "GetSeasons\x12!.prairie.plugin.v1.GetSeasonsRequest\x1a\".prairie.plugin.v1.GetSeasonsResponse\x12V\n" + - "\vGetEpisodes\x12\".prairie.plugin.v1.GetEpisodesRequest\x1a#.prairie.plugin.v1.GetEpisodesResponse\x12P\n" + - "\tGetImages\x12 .prairie.plugin.v1.GetImagesRequest\x1a!.prairie.plugin.v1.GetImagesResponse\x12b\n" + - "\x0fResolveImageURL\x12&.prairie.plugin.v1.ResolveImageURLRequest\x1a'.prairie.plugin.v1.ResolveImageURLResponse\x12e\n" + - "\x10ResolveImageURLs\x12'.prairie.plugin.v1.ResolveImageURLsRequest\x1a(.prairie.plugin.v1.ResolveImageURLsResponse2\xda\x01\n" + - "\rImageResolver\x12b\n" + - "\x0fResolveImageURL\x12&.prairie.plugin.v1.ResolveImageURLRequest\x1a'.prairie.plugin.v1.ResolveImageURLResponse\x12e\n" + - "\x10ResolveImageURLs\x12'.prairie.plugin.v1.ResolveImageURLsRequest\x1a(.prairie.plugin.v1.ResolveImageURLsResponseBPZNgithub.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1;pluginv1b\x06proto3" + "GetSeasons\x12$.prairie.plugin.v1.GetSeasonsRequest\x1a%.prairie.plugin.v1.GetSeasonsResponse\x12\\\n" + + "\vGetEpisodes\x12%.prairie.plugin.v1.GetEpisodesRequest\x1a&.prairie.plugin.v1.GetEpisodesResponse\x12V\n" + + "\tGetImages\x12#.prairie.plugin.v1.GetImagesRequest\x1a$.prairie.plugin.v1.GetImagesResponse\x12h\n" + + "\x0fResolveImageURL\x12).prairie.plugin.v1.ResolveImageURLRequest\x1a*.prairie.plugin.v1.ResolveImageURLResponse\x12k\n" + + "\x10ResolveImageURLs\x12*.prairie.plugin.v1.ResolveImageURLsRequest\x1a+.prairie.plugin.v1.ResolveImageURLsResponse2\xe6\x01\n" + + "\rImageResolver\x12h\n" + + "\x0fResolveImageURL\x12).prairie.plugin.v1.ResolveImageURLRequest\x1a*.prairie.plugin.v1.ResolveImageURLResponse\x12k\n" + + "\x10ResolveImageURLs\x12*.prairie.plugin.v1.ResolveImageURLsRequest\x1a+.prairie.plugin.v1.ResolveImageURLsResponseBYZWgithub.com/prairie-server/prairie-plugin-sdk/pkg/pluginproto/prairie/plugin/v1;pluginv1b\x06proto3" var ( file_prairie_plugin_v1_metadata_provider_proto_rawDescOnce sync.Once