diff --git a/js/docs/src/content/docs/lex-reference/media/place-stream-media-finalizelivestream.md b/js/docs/src/content/docs/lex-reference/media/place-stream-media-finalizelivestream.md index 79e8f4f08..fa896206f 100644 --- a/js/docs/src/content/docs/lex-reference/media/place-stream-media-finalizelivestream.md +++ b/js/docs/src/content/docs/lex-reference/media/place-stream-media-finalizelivestream.md @@ -13,7 +13,7 @@ description: Reference for the place.stream.media.finalizeLivestream lexicon **Type:** `procedure` -Turn a finished livestream into a VOD. The server concatenates the MUXL segments it recorded for the livestream into a single content blob, derives the playback sidecars, and publishes the place.stream.media.track records — the same end state as a finished upload. Returns an uploadId the client polls with place.stream.media.getUploadStatus and then publishes with place.stream.media.publishVideo, exactly as for a resumable upload. +Turn a finished livestream into a VOD. The server concatenates the MUXL segments it recorded for the livestream into a single content blob, derives the playback sidecars, and publishes the place.stream.media.track records. Processing completes server-side and creates a draft VOD; the client does not need to poll or publish — the user publishes the draft later via place.stream.vod.publishDraft. Returns the draft's ats:// URI so the client can navigate to it, alongside an uploadId for backwards compatibility. **Parameters:** _(None defined)_ @@ -35,9 +35,10 @@ Turn a finished livestream into a VOD. The server concatenates the MUXL segments **Schema Type:** `object` -| Name | Type | Req'd | Description | Constraints | -| ---------- | -------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -| `uploadId` | `string` | ✅ | Identifier for the finalize job. Poll place.stream.media.getUploadStatus with it; once status is 'done', create the video with place.stream.media.publishVideo. | | +| Name | Type | Req'd | Description | Constraints | +| ---------- | -------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| `uploadId` | `string` | ✅ | Identifier for the finalize job. Retained for backwards compatibility; the draft flow no longer requires the client to poll getUploadStatus. | | +| `draftUri` | `string` | ✅ | The ats:// URI of the draft VOD created for this finalize. The draft reaches status 'ready' when processing completes; the user publishes it from the Drafts tab via place.stream.vod.publishDraft. | | **Possible Errors:** @@ -55,7 +56,7 @@ Turn a finished livestream into a VOD. The server concatenates the MUXL segments "defs": { "main": { "type": "procedure", - "description": "Turn a finished livestream into a VOD. The server concatenates the MUXL segments it recorded for the livestream into a single content blob, derives the playback sidecars, and publishes the place.stream.media.track records — the same end state as a finished upload. Returns an uploadId the client polls with place.stream.media.getUploadStatus and then publishes with place.stream.media.publishVideo, exactly as for a resumable upload.", + "description": "Turn a finished livestream into a VOD. The server concatenates the MUXL segments it recorded for the livestream into a single content blob, derives the playback sidecars, and publishes the place.stream.media.track records. Processing completes server-side and creates a draft VOD; the client does not need to poll or publish — the user publishes the draft later via place.stream.vod.publishDraft. Returns the draft's ats:// URI so the client can navigate to it, alongside an uploadId for backwards compatibility.", "input": { "encoding": "application/json", "schema": { @@ -74,11 +75,15 @@ Turn a finished livestream into a VOD. The server concatenates the MUXL segments "encoding": "application/json", "schema": { "type": "object", - "required": ["uploadId"], + "required": ["uploadId", "draftUri"], "properties": { "uploadId": { "type": "string", - "description": "Identifier for the finalize job. Poll place.stream.media.getUploadStatus with it; once status is 'done', create the video with place.stream.media.publishVideo." + "description": "Identifier for the finalize job. Retained for backwards compatibility; the draft flow no longer requires the client to poll getUploadStatus." + }, + "draftUri": { + "type": "string", + "description": "The ats:// URI of the draft VOD created for this finalize. The draft reaches status 'ready' when processing completes; the user publishes it from the Drafts tab via place.stream.vod.publishDraft." } } } diff --git a/js/docs/src/content/docs/lex-reference/openapi.json b/js/docs/src/content/docs/lex-reference/openapi.json index 7f440acb7..2b9906132 100644 --- a/js/docs/src/content/docs/lex-reference/openapi.json +++ b/js/docs/src/content/docs/lex-reference/openapi.json @@ -82,6 +82,59 @@ ] } }, + "/xrpc/place.stream.vod.deleteDraft": { + "post": { + "summary": "Discard a draft VOD. The processed content blob is not deleted (it may be referenced elsewhere). Only the draft record is removed.", + "operationId": "place.stream.vod.deleteDraft", + "tags": ["place.stream.vod"], + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["error", "message"], + "properties": { + "error": { + "type": "string", + "oneOf": [ + { + "const": "NotFound" + } + ] + }, + "message": { + "type": "string" + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "The ats:// URI of the draft to discard." + } + }, + "required": ["uri"] + } + } + } + } + } + }, "/xrpc/place.stream.vod.getComments": { "get": { "summary": "Get comments for a place.stream.video record.", @@ -149,6 +202,308 @@ ] } }, + "/xrpc/place.stream.vod.getDraft": { + "get": { + "summary": "Get a single draft VOD by its ats:// URI. Only accessible by the draft's author.", + "operationId": "place.stream.vod.getDraft", + "tags": ["place.stream.vod"], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "draft": { + "$ref": "#/components/schemas/place.stream.vod.draftDefs_draftView" + } + }, + "required": ["draft"] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["error", "message"], + "properties": { + "error": { + "type": "string", + "oneOf": [ + { + "const": "NotFound" + } + ] + }, + "message": { + "type": "string" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "uri", + "in": "query", + "required": true, + "description": "The ats:// URI of the draft record.", + "schema": { + "type": "string", + "description": "The ats:// URI of the draft record." + } + } + ] + } + }, + "/xrpc/place.stream.vod.listDrafts": { + "get": { + "summary": "List the authenticated user's draft VODs, newest first.", + "operationId": "place.stream.vod.listDrafts", + "tags": ["place.stream.vod"], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "drafts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/place.stream.vod.draftDefs_draftView" + } + }, + "cursor": { + "type": "string", + "description": "Pagination cursor for the next page, if more results exist." + } + }, + "required": ["drafts"] + } + } + } + } + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "description": "Number of drafts to return.", + "schema": { + "type": "integer", + "description": "Number of drafts to return.", + "default": 50, + "minimum": 1, + "maximum": 100 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "description": "Pagination cursor from a previous response.", + "schema": { + "type": "string", + "description": "Pagination cursor from a previous response." + } + } + ] + } + }, + "/xrpc/place.stream.vod.publishDraft": { + "post": { + "summary": "Promote a ready draft VOD to a public place.stream.video record. The server carries over source and durationMs from the draft, backfills a generated thumbnail if the draft has none, writes the video record via putRecord, then deletes the draft.", + "operationId": "place.stream.vod.publishDraft", + "tags": ["place.stream.vod"], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "videoUri": { + "type": "string", + "description": "AT-URI of the created place.stream.video record.", + "format": "uri" + }, + "videoCid": { + "type": "string", + "description": "CID of the created place.stream.video record.", + "format": "cid" + } + }, + "required": ["videoUri", "videoCid"] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["error", "message"], + "properties": { + "error": { + "type": "string", + "oneOf": [ + { + "const": "NotFound" + }, + { + "const": "NotReady" + } + ] + }, + "message": { + "type": "string" + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "The ats:// URI of the draft to publish. Must have status 'ready'." + } + }, + "required": ["uri"] + } + } + } + } + } + }, + "/xrpc/place.stream.vod.updateDraft": { + "post": { + "summary": "Update a draft VOD's editable metadata. Server-authoritative fields (source, durationMs, status) are preserved. Editing is allowed in any status, so the user can pre-fill metadata while processing. All fields except uri are optional (partial update).", + "operationId": "place.stream.vod.updateDraft", + "tags": ["place.stream.vod"], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "draft": { + "$ref": "#/components/schemas/place.stream.vod.draftDefs_draftView" + } + }, + "required": ["draft"] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["error", "message"], + "properties": { + "error": { + "type": "string", + "oneOf": [ + { + "const": "NotFound" + } + ] + }, + "message": { + "type": "string" + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "The ats:// URI of the draft to update." + }, + "title": { + "type": "string", + "maxLength": 1400 + }, + "description": { + "type": "string", + "maxLength": 50000 + }, + "descriptionFacets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/place.stream.richtext.videoFacet" + } + }, + "thumb": { + "type": "string", + "format": "binary" + }, + "activity": { + "oneOf": [ + { + "$ref": "#/components/schemas/place.stream.defs_activityGame" + }, + { + "$ref": "#/components/schemas/place.stream.defs_activityLabel" + } + ] + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "maxLength": 640 + }, + "maxItems": 10 + }, + "contentWarnings": { + "$ref": "#/components/schemas/place.stream.metadata.contentWarnings" + }, + "contentRights": { + "$ref": "#/components/schemas/place.stream.metadata.contentRights" + } + }, + "required": ["uri"] + } + } + } + } + } + }, "/xrpc/place.stream.server.createWebhook": { "post": { "summary": "Create a new webhook for receiving Streamplace events.", @@ -2288,7 +2643,7 @@ }, "/xrpc/place.stream.media.finalizeLivestream": { "post": { - "summary": "Turn a finished livestream into a VOD. The server concatenates the MUXL segments it recorded for the livestream into a single content blob, derives the playback sidecars, and publishes the place.stream.media.track records — the same end state as a finished upload. Returns an uploadId the client polls with place.stream.media.getUploadStatus and then publishes with place.stream.media.publishVideo, exactly as for a resumable upload.", + "summary": "Turn a finished livestream into a VOD. The server concatenates the MUXL segments it recorded for the livestream into a single content blob, derives the playback sidecars, and publishes the place.stream.media.track records. Processing completes server-side and creates a draft VOD; the client does not need to poll or publish — the user publishes the draft later via place.stream.vod.publishDraft. Returns the draft's ats:// URI so the client can navigate to it, alongside an uploadId for backwards compatibility.", "operationId": "place.stream.media.finalizeLivestream", "tags": ["place.stream.media"], "responses": { @@ -2301,10 +2656,14 @@ "properties": { "uploadId": { "type": "string", - "description": "Identifier for the finalize job. Poll place.stream.media.getUploadStatus with it; once status is 'done', create the video with place.stream.media.publishVideo." + "description": "Identifier for the finalize job. Retained for backwards compatibility; the draft flow no longer requires the client to poll getUploadStatus." + }, + "draftUri": { + "type": "string", + "description": "The ats:// URI of the draft VOD created for this finalize. The draft reaches status 'ready' when processing completes; the user publishes it from the Drafts tab via place.stream.vod.publishDraft." } }, - "required": ["uploadId"] + "required": ["uploadId", "draftUri"] } } } @@ -5424,6 +5783,150 @@ }, "required": ["uri", "cid", "author", "record", "indexedAt", "likeCount"] }, + "place.stream.vod.draftDefs_draftView": { + "type": "object", + "description": "A draft VOD with its ats:// URI and CID, for list/get responses.", + "properties": { + "uri": { + "type": "string", + "description": "The ats:// URI of the draft record." + }, + "cid": { + "type": "string", + "description": "CID (sha256 of CBOR record bytes, base32) of the draft record." + }, + "record": { + "$ref": "#/components/schemas/place.stream.vod.draftVideo" + } + }, + "required": ["uri", "cid", "record"] + }, + "place.stream.vod.draftVideo": { + "description": "Unknown type" + }, + "place.stream.richtext.videoFacet": { + "type": "object", + "description": "Annotation of a sub-string within rich text in a VOD description or comment.", + "properties": { + "index": { + "$ref": "#/components/schemas/app.bsky.richtext.facet_byteSlice" + }, + "features": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/app.bsky.richtext.facet_mention" + }, + { + "$ref": "#/components/schemas/app.bsky.richtext.facet_link" + } + ] + } + } + }, + "required": ["index", "features"] + }, + "app.bsky.richtext.facet_byteSlice": { + "type": "object", + "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.", + "properties": { + "byteStart": { + "type": "integer", + "minimum": 0 + }, + "byteEnd": { + "type": "integer", + "minimum": 0 + } + }, + "required": ["byteStart", "byteEnd"] + }, + "app.bsky.richtext.facet_mention": { + "type": "object", + "description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.", + "properties": { + "did": { + "type": "string", + "format": "did" + } + }, + "required": ["did"] + }, + "app.bsky.richtext.facet_link": { + "type": "object", + "description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.", + "properties": { + "uri": { + "type": "string", + "format": "uri" + } + }, + "required": ["uri"] + }, + "place.stream.defs_activityGame": { + "type": "object", + "description": "A game from the gamesgamesgamesgames catalog, identified by its AT URI.", + "properties": { + "uri": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string", + "description": "Cached display name of the game." + } + }, + "required": ["uri"] + }, + "place.stream.defs_activityLabel": { + "type": "object", + "description": "A non-game activity with a well-known label.", + "properties": { + "label": { + "type": "string" + } + }, + "required": ["label"] + }, + "place.stream.metadata.contentWarnings": { + "type": "object", + "description": "Content warnings for a stream.", + "properties": { + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "place.stream.metadata.contentRights": { + "type": "object", + "description": "Content rights and attribution information.", + "properties": { + "creator": { + "type": "string", + "description": "Name of the creator of the work." + }, + "copyrightNotice": { + "type": "string", + "description": "Copyright notice for the work." + }, + "copyrightYear": { + "type": "integer", + "description": "Year of creation or publication." + }, + "license": { + "type": "string", + "description": "License URL or identifier." + }, + "creditLine": { + "type": "string", + "description": "Credit line for the work." + } + } + }, "place.stream.server.defs_rewriteRule": { "type": "object", "properties": { diff --git a/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-deletedraft.md b/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-deletedraft.md new file mode 100644 index 000000000..2aa641573 --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-deletedraft.md @@ -0,0 +1,69 @@ +--- +title: place.stream.vod.deleteDraft +description: Reference for the place.stream.vod.deleteDraft lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `procedure` + +Discard a draft VOD. The processed content blob is not deleted (it may be referenced elsewhere). Only the draft record is removed. + +**Parameters:** _(None defined)_ + +**Input:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| ----- | -------- | ----- | --------------------------------------- | ----------- | +| `uri` | `string` | ✅ | The ats:// URI of the draft to discard. | | + +**Possible Errors:** + +- `NotFound`: No draft exists with the given URI for the authenticated user. + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.vod.deleteDraft", + "defs": { + "main": { + "type": "procedure", + "description": "Discard a draft VOD. The processed content blob is not deleted (it may be referenced elsewhere). Only the draft record is removed.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "description": "The ats:// URI of the draft to discard." + } + } + } + }, + "errors": [ + { + "name": "NotFound", + "description": "No draft exists with the given URI for the authenticated user." + } + ] + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-draftdefs.md b/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-draftdefs.md new file mode 100644 index 000000000..e00b1ebed --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-draftdefs.md @@ -0,0 +1,56 @@ +--- +title: place.stream.vod.draftDefs +description: Reference for the place.stream.vod.draftDefs lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `draftView` + +**Type:** `object` + +A draft VOD with its ats:// URI and CID, for list/get responses. + +**Properties:** + +| Name | Type | Req'd | Description | Constraints | +| -------- | --------------------------------------------------------------------------- | ----- | -------------------------------------------------------------- | ----------- | +| `uri` | `string` | ✅ | The ats:// URI of the draft record. | | +| `cid` | `string` | ✅ | CID (sha256 of CBOR record bytes, base32) of the draft record. | | +| `record` | [`place.stream.vod.draftVideo`](/lex-reference/place-stream-vod-draftvideo) | ✅ | | | + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.vod.draftDefs", + "defs": { + "draftView": { + "type": "object", + "description": "A draft VOD with its ats:// URI and CID, for list/get responses.", + "required": ["uri", "cid", "record"], + "properties": { + "uri": { + "type": "string", + "description": "The ats:// URI of the draft record." + }, + "cid": { + "type": "string", + "description": "CID (sha256 of CBOR record bytes, base32) of the draft record." + }, + "record": { + "type": "ref", + "ref": "place.stream.vod.draftVideo" + } + } + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-draftvideo.md b/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-draftvideo.md new file mode 100644 index 000000000..eadd2be95 --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-draftvideo.md @@ -0,0 +1,146 @@ +--- +title: place.stream.vod.draftVideo +description: Reference for the place.stream.vod.draftVideo lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `record` + +A draft VOD: pre-publication video metadata and processing state. Modeled as a permissioned-data-style record (CBOR-serialized, addressed by an ats:// URI) stored in statedb until the permissioned-data spec ships (bluesky-social/proposals PR #94, the 0016-permissioned-data proposal). Publishing promotes it to a public place.stream.video record. + +**Record Key:** `tid` + +**Record Properties:** + +| Name | Type | Req'd | Description | Constraints | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------- | +| `title` | `string` | ✅ | Title of the video. May be a placeholder while processing; the user edits before publishing. | Max Length: 1400
Max Graphemes: 140 | +| `description` | `string` | ❌ | Description of the video. | Max Length: 50000
Max Graphemes: 5000 | +| `descriptionFacets` | Array of [`place.stream.richtext.videoFacet`](/lex-reference/place-stream-richtext-videofacet) | ❌ | Annotations of text (mentions, URLs, etc). | | +| `thumb` | `blob` | ❌ | Thumbnail image. Generated by the server at publish time if absent. | Accept: `image/*`
Max Size: 1000000 bytes | +| `source` | Union of:
  [`place.stream.media.defs#sourceTracks`](/lex-reference/place-stream-media-defs#sourcetracks)
  [`place.stream.media.defs#sourceClip`](/lex-reference/place-stream-media-defs#sourceclip) | ❌ | Source tracks. Filled by the server when processing completes (status 'ready'). | | +| `durationMs` | `integer` | ❌ | Duration in milliseconds. Filled when status is 'ready'. | | +| `activity` | Union of:
  [`place.stream.defs#activityGame`](/lex-reference/place-stream-defs#activitygame)
  [`place.stream.defs#activityLabel`](/lex-reference/place-stream-defs#activitylabel) | ❌ | The game or activity in the video. | | +| `tags` | Array of `string` | ❌ | Freeform tags (same rules as place.stream.video). | Max Items: 10 | +| `connections` | Array of Union of:
  [`place.stream.video#connection`](/lex-reference/place-stream-video#connection) | ❌ | Related records (e.g. the source livestream for a finalized VOD). | | +| `contentWarnings` | [`place.stream.metadata.contentWarnings`](/lex-reference/place-stream-metadata-contentwarnings) | ❌ | | | +| `contentRights` | [`place.stream.metadata.contentRights`](/lex-reference/place-stream-metadata-contentrights) | ❌ | | | +| `status` | `string` | ✅ | Processing state: 'processing' while the job runs, 'ready' when publishable, 'error' if processing failed. | Known Values: `processing`, `ready`, `error` | +| `error` | `string` | ❌ | Error message when status is 'error'. | | +| `createdAt` | `string` | ✅ | | Format: `datetime` | + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.vod.draftVideo", + "defs": { + "main": { + "type": "record", + "key": "tid", + "description": "A draft VOD: pre-publication video metadata and processing state. Modeled as a permissioned-data-style record (CBOR-serialized, addressed by an ats:// URI) stored in statedb until the permissioned-data spec ships (bluesky-social/proposals PR #94, the 0016-permissioned-data proposal). Publishing promotes it to a public place.stream.video record.", + "record": { + "type": "object", + "required": ["title", "status", "createdAt"], + "properties": { + "title": { + "type": "string", + "maxLength": 1400, + "maxGraphemes": 140, + "description": "Title of the video. May be a placeholder while processing; the user edits before publishing." + }, + "description": { + "type": "string", + "maxLength": 50000, + "maxGraphemes": 5000, + "description": "Description of the video." + }, + "descriptionFacets": { + "type": "array", + "description": "Annotations of text (mentions, URLs, etc).", + "items": { + "type": "ref", + "ref": "place.stream.richtext.videoFacet" + } + }, + "thumb": { + "type": "blob", + "accept": ["image/*"], + "maxSize": 1000000, + "description": "Thumbnail image. Generated by the server at publish time if absent." + }, + "source": { + "type": "union", + "refs": [ + "place.stream.media.defs#sourceTracks", + "place.stream.media.defs#sourceClip" + ], + "description": "Source tracks. Filled by the server when processing completes (status 'ready')." + }, + "durationMs": { + "type": "integer", + "description": "Duration in milliseconds. Filled when status is 'ready'." + }, + "activity": { + "type": "union", + "description": "The game or activity in the video.", + "refs": [ + "place.stream.defs#activityGame", + "place.stream.defs#activityLabel" + ] + }, + "tags": { + "type": "array", + "maxLength": 10, + "description": "Freeform tags (same rules as place.stream.video).", + "items": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + } + }, + "connections": { + "type": "array", + "description": "Related records (e.g. the source livestream for a finalized VOD).", + "items": { + "type": "union", + "refs": ["place.stream.video#connection"] + } + }, + "contentWarnings": { + "type": "ref", + "ref": "place.stream.metadata.contentWarnings" + }, + "contentRights": { + "type": "ref", + "ref": "place.stream.metadata.contentRights" + }, + "status": { + "type": "string", + "knownValues": ["processing", "ready", "error"], + "description": "Processing state: 'processing' while the job runs, 'ready' when publishable, 'error' if processing failed." + }, + "error": { + "type": "string", + "description": "Error message when status is 'error'." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-getdraft.md b/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-getdraft.md new file mode 100644 index 000000000..6712a5796 --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-getdraft.md @@ -0,0 +1,83 @@ +--- +title: place.stream.vod.getDraft +description: Reference for the place.stream.vod.getDraft lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `query` + +Get a single draft VOD by its ats:// URI. Only accessible by the draft's author. + +**Parameters:** + +| Name | Type | Req'd | Description | Constraints | +| ----- | -------- | ----- | ----------------------------------- | ----------- | +| `uri` | `string` | ✅ | The ats:// URI of the draft record. | | + +**Output:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| ------- | --------------------------------------------------------------------------------------------- | ----- | ----------- | ----------- | +| `draft` | [`place.stream.vod.draftDefs#draftView`](/lex-reference/place-stream-vod-draftdefs#draftview) | ✅ | | | + +**Possible Errors:** + +- `NotFound`: No draft exists with the given URI for the authenticated user. + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.vod.getDraft", + "defs": { + "main": { + "type": "query", + "description": "Get a single draft VOD by its ats:// URI. Only accessible by the draft's author.", + "parameters": { + "type": "params", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "description": "The ats:// URI of the draft record." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["draft"], + "properties": { + "draft": { + "type": "ref", + "ref": "place.stream.vod.draftDefs#draftView" + } + } + } + }, + "errors": [ + { + "name": "NotFound", + "description": "No draft exists with the given URI for the authenticated user." + } + ] + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-listdrafts.md b/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-listdrafts.md new file mode 100644 index 000000000..5209c94d6 --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-listdrafts.md @@ -0,0 +1,88 @@ +--- +title: place.stream.vod.listDrafts +description: Reference for the place.stream.vod.listDrafts lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `query` + +List the authenticated user's draft VODs, newest first. + +**Parameters:** + +| Name | Type | Req'd | Description | Constraints | +| -------- | --------- | ----- | ------------------------------------------- | ------------------------------------- | +| `limit` | `integer` | ❌ | Number of drafts to return. | Min: 1
Max: 100
Default: `50` | +| `cursor` | `string` | ❌ | Pagination cursor from a previous response. | | + +**Output:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| -------- | ------------------------------------------------------------------------------------------------------ | ----- | ----------------------------------------------------------- | ----------- | +| `drafts` | Array of [`place.stream.vod.draftDefs#draftView`](/lex-reference/place-stream-vod-draftdefs#draftview) | ✅ | | | +| `cursor` | `string` | ❌ | Pagination cursor for the next page, if more results exist. | | + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.vod.listDrafts", + "defs": { + "main": { + "type": "query", + "description": "List the authenticated user's draft VODs, newest first.", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50, + "description": "Number of drafts to return." + }, + "cursor": { + "type": "string", + "description": "Pagination cursor from a previous response." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["drafts"], + "properties": { + "drafts": { + "type": "array", + "items": { + "type": "ref", + "ref": "place.stream.vod.draftDefs#draftView" + } + }, + "cursor": { + "type": "string", + "description": "Pagination cursor for the next page, if more results exist." + } + } + } + } + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-publishdraft.md b/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-publishdraft.md new file mode 100644 index 000000000..f4ecf95d1 --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-publishdraft.md @@ -0,0 +1,105 @@ +--- +title: place.stream.vod.publishDraft +description: Reference for the place.stream.vod.publishDraft lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `procedure` + +Promote a ready draft VOD to a public place.stream.video record. The server carries over source and durationMs from the draft, backfills a generated thumbnail if the draft has none, writes the video record via putRecord, then deletes the draft. + +**Parameters:** _(None defined)_ + +**Input:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| ----- | -------- | ----- | ----------------------------------------------------------------- | ----------- | +| `uri` | `string` | ✅ | The ats:// URI of the draft to publish. Must have status 'ready'. | | + +**Output:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| ---------- | -------- | ----- | ------------------------------------------------ | ---------------- | +| `videoUri` | `string` | ✅ | AT-URI of the created place.stream.video record. | Format: `at-uri` | +| `videoCid` | `string` | ✅ | CID of the created place.stream.video record. | Format: `cid` | + +**Possible Errors:** + +- `NotFound`: No draft exists with the given URI for the authenticated user. +- `NotReady`: The draft is not ready to publish (status is 'processing' or 'error'). + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.vod.publishDraft", + "defs": { + "main": { + "type": "procedure", + "description": "Promote a ready draft VOD to a public place.stream.video record. The server carries over source and durationMs from the draft, backfills a generated thumbnail if the draft has none, writes the video record via putRecord, then deletes the draft.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "description": "The ats:// URI of the draft to publish. Must have status 'ready'." + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["videoUri", "videoCid"], + "properties": { + "videoUri": { + "type": "string", + "format": "at-uri", + "description": "AT-URI of the created place.stream.video record." + }, + "videoCid": { + "type": "string", + "format": "cid", + "description": "CID of the created place.stream.video record." + } + } + } + }, + "errors": [ + { + "name": "NotFound", + "description": "No draft exists with the given URI for the authenticated user." + }, + { + "name": "NotReady", + "description": "The draft is not ready to publish (status is 'processing' or 'error')." + } + ] + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-updatedraft.md b/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-updatedraft.md new file mode 100644 index 000000000..c067fe14d --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/vod/place-stream-vod-updatedraft.md @@ -0,0 +1,147 @@ +--- +title: place.stream.vod.updateDraft +description: Reference for the place.stream.vod.updateDraft lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `procedure` + +Update a draft VOD's editable metadata. Server-authoritative fields (source, durationMs, status) are preserved. Editing is allowed in any status, so the user can pre-fill metadata while processing. All fields except uri are optional (partial update). + +**Parameters:** _(None defined)_ + +**Input:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -------------------------------------- | --------------------------------------------- | +| `uri` | `string` | ✅ | The ats:// URI of the draft to update. | | +| `title` | `string` | ❌ | | Max Length: 1400
Max Graphemes: 140 | +| `description` | `string` | ❌ | | Max Length: 50000
Max Graphemes: 5000 | +| `descriptionFacets` | Array of [`place.stream.richtext.videoFacet`](/lex-reference/place-stream-richtext-videofacet) | ❌ | | | +| `thumb` | `blob` | ❌ | | Accept: `image/*`
Max Size: 1000000 bytes | +| `activity` | Union of:
  [`place.stream.defs#activityGame`](/lex-reference/place-stream-defs#activitygame)
  [`place.stream.defs#activityLabel`](/lex-reference/place-stream-defs#activitylabel) | ❌ | | | +| `tags` | Array of `string` | ❌ | | Max Items: 10 | +| `contentWarnings` | [`place.stream.metadata.contentWarnings`](/lex-reference/place-stream-metadata-contentwarnings) | ❌ | | | +| `contentRights` | [`place.stream.metadata.contentRights`](/lex-reference/place-stream-metadata-contentrights) | ❌ | | | + +**Output:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| ------- | --------------------------------------------------------------------------------------------- | ----- | ----------- | ----------- | +| `draft` | [`place.stream.vod.draftDefs#draftView`](/lex-reference/place-stream-vod-draftdefs#draftview) | ✅ | | | + +**Possible Errors:** + +- `NotFound`: No draft exists with the given URI for the authenticated user. + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.vod.updateDraft", + "defs": { + "main": { + "type": "procedure", + "description": "Update a draft VOD's editable metadata. Server-authoritative fields (source, durationMs, status) are preserved. Editing is allowed in any status, so the user can pre-fill metadata while processing. All fields except uri are optional (partial update).", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "description": "The ats:// URI of the draft to update." + }, + "title": { + "type": "string", + "maxLength": 1400, + "maxGraphemes": 140 + }, + "description": { + "type": "string", + "maxLength": 50000, + "maxGraphemes": 5000 + }, + "descriptionFacets": { + "type": "array", + "items": { + "type": "ref", + "ref": "place.stream.richtext.videoFacet" + } + }, + "thumb": { + "type": "blob", + "accept": ["image/*"], + "maxSize": 1000000 + }, + "activity": { + "type": "union", + "refs": [ + "place.stream.defs#activityGame", + "place.stream.defs#activityLabel" + ] + }, + "tags": { + "type": "array", + "maxLength": 10, + "items": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + } + }, + "contentWarnings": { + "type": "ref", + "ref": "place.stream.metadata.contentWarnings" + }, + "contentRights": { + "type": "ref", + "ref": "place.stream.metadata.contentRights" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["draft"], + "properties": { + "draft": { + "type": "ref", + "ref": "place.stream.vod.draftDefs#draftView" + } + } + } + }, + "errors": [ + { + "name": "NotFound", + "description": "No draft exists with the given URI for the authenticated user." + } + ] + } + } +} +``` diff --git a/lexicons/place/stream/media/finalizeLivestream.json b/lexicons/place/stream/media/finalizeLivestream.json index 5c8c96af6..615234b86 100644 --- a/lexicons/place/stream/media/finalizeLivestream.json +++ b/lexicons/place/stream/media/finalizeLivestream.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Turn a finished livestream into a VOD. The server concatenates the MUXL segments it recorded for the livestream into a single content blob, derives the playback sidecars, and publishes the place.stream.media.track records — the same end state as a finished upload. Returns an uploadId the client polls with place.stream.media.getUploadStatus and then publishes with place.stream.media.publishVideo, exactly as for a resumable upload.", + "description": "Turn a finished livestream into a VOD. The server concatenates the MUXL segments it recorded for the livestream into a single content blob, derives the playback sidecars, and publishes the place.stream.media.track records. Processing completes server-side and creates a draft VOD; the client does not need to poll or publish — the user publishes the draft later via place.stream.vod.publishDraft. Returns the draft's ats:// URI so the client can navigate to it, alongside an uploadId for backwards compatibility.", "input": { "encoding": "application/json", "schema": { @@ -23,11 +23,15 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["uploadId"], + "required": ["uploadId", "draftUri"], "properties": { "uploadId": { "type": "string", - "description": "Identifier for the finalize job. Poll place.stream.media.getUploadStatus with it; once status is 'done', create the video with place.stream.media.publishVideo." + "description": "Identifier for the finalize job. Retained for backwards compatibility; the draft flow no longer requires the client to poll getUploadStatus." + }, + "draftUri": { + "type": "string", + "description": "The ats:// URI of the draft VOD created for this finalize. The draft reaches status 'ready' when processing completes; the user publishes it from the Drafts tab via place.stream.vod.publishDraft." } } } diff --git a/lexicons/place/stream/vod/deleteDraft.json b/lexicons/place/stream/vod/deleteDraft.json new file mode 100644 index 000000000..51c38da48 --- /dev/null +++ b/lexicons/place/stream/vod/deleteDraft.json @@ -0,0 +1,29 @@ +{ + "lexicon": 1, + "id": "place.stream.vod.deleteDraft", + "defs": { + "main": { + "type": "procedure", + "description": "Discard a draft VOD. The processed content blob is not deleted (it may be referenced elsewhere). Only the draft record is removed.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "description": "The ats:// URI of the draft to discard." + } + } + } + }, + "errors": [ + { + "name": "NotFound", + "description": "No draft exists with the given URI for the authenticated user." + } + ] + } + } +} diff --git a/lexicons/place/stream/vod/draftDefs.json b/lexicons/place/stream/vod/draftDefs.json new file mode 100644 index 000000000..6ebb794b0 --- /dev/null +++ b/lexicons/place/stream/vod/draftDefs.json @@ -0,0 +1,25 @@ +{ + "lexicon": 1, + "id": "place.stream.vod.draftDefs", + "defs": { + "draftView": { + "type": "object", + "description": "A draft VOD with its ats:// URI and CID, for list/get responses.", + "required": ["uri", "cid", "record"], + "properties": { + "uri": { + "type": "string", + "description": "The ats:// URI of the draft record." + }, + "cid": { + "type": "string", + "description": "CID (sha256 of CBOR record bytes, base32) of the draft record." + }, + "record": { + "type": "ref", + "ref": "place.stream.vod.draftVideo" + } + } + } + } +} diff --git a/lexicons/place/stream/vod/draftVideo.json b/lexicons/place/stream/vod/draftVideo.json new file mode 100644 index 000000000..48235154d --- /dev/null +++ b/lexicons/place/stream/vod/draftVideo.json @@ -0,0 +1,95 @@ +{ + "lexicon": 1, + "id": "place.stream.vod.draftVideo", + "defs": { + "main": { + "type": "record", + "key": "tid", + "description": "A draft VOD: pre-publication video metadata and processing state. Modeled as a permissioned-data-style record (CBOR-serialized, addressed by an ats:// URI) stored in statedb until the permissioned-data spec ships (bluesky-social/proposals PR #94, the 0016-permissioned-data proposal). Publishing promotes it to a public place.stream.video record.", + "record": { + "type": "object", + "required": ["title", "status", "createdAt"], + "properties": { + "title": { + "type": "string", + "maxLength": 1400, + "maxGraphemes": 140, + "description": "Title of the video. May be a placeholder while processing; the user edits before publishing." + }, + "description": { + "type": "string", + "maxLength": 50000, + "maxGraphemes": 5000, + "description": "Description of the video." + }, + "descriptionFacets": { + "type": "array", + "description": "Annotations of text (mentions, URLs, etc).", + "items": { + "type": "ref", + "ref": "place.stream.richtext.videoFacet" + } + }, + "thumb": { + "type": "blob", + "accept": ["image/*"], + "maxSize": 1000000, + "description": "Thumbnail image. Generated by the server at publish time if absent." + }, + "source": { + "type": "union", + "refs": [ + "place.stream.media.defs#sourceTracks", + "place.stream.media.defs#sourceClip" + ], + "description": "Source tracks. Filled by the server when processing completes (status 'ready')." + }, + "durationMs": { + "type": "integer", + "description": "Duration in milliseconds. Filled when status is 'ready'." + }, + "activity": { + "type": "union", + "description": "The game or activity in the video.", + "refs": [ + "place.stream.defs#activityGame", + "place.stream.defs#activityLabel" + ] + }, + "tags": { + "type": "array", + "maxLength": 10, + "description": "Freeform tags (same rules as place.stream.video).", + "items": { "type": "string", "maxLength": 640, "maxGraphemes": 64 } + }, + "connections": { + "type": "array", + "description": "Related records (e.g. the source livestream for a finalized VOD).", + "items": { + "type": "union", + "refs": ["place.stream.video#connection"] + } + }, + "contentWarnings": { + "type": "ref", + "ref": "place.stream.metadata.contentWarnings" + }, + "contentRights": { + "type": "ref", + "ref": "place.stream.metadata.contentRights" + }, + "status": { + "type": "string", + "knownValues": ["processing", "ready", "error"], + "description": "Processing state: 'processing' while the job runs, 'ready' when publishable, 'error' if processing failed." + }, + "error": { + "type": "string", + "description": "Error message when status is 'error'." + }, + "createdAt": { "type": "string", "format": "datetime" } + } + } + } + } +} diff --git a/lexicons/place/stream/vod/getDraft.json b/lexicons/place/stream/vod/getDraft.json new file mode 100644 index 000000000..db39b300a --- /dev/null +++ b/lexicons/place/stream/vod/getDraft.json @@ -0,0 +1,39 @@ +{ + "lexicon": 1, + "id": "place.stream.vod.getDraft", + "defs": { + "main": { + "type": "query", + "description": "Get a single draft VOD by its ats:// URI. Only accessible by the draft's author.", + "parameters": { + "type": "params", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "description": "The ats:// URI of the draft record." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["draft"], + "properties": { + "draft": { + "type": "ref", + "ref": "place.stream.vod.draftDefs#draftView" + } + } + } + }, + "errors": [ + { + "name": "NotFound", + "description": "No draft exists with the given URI for the authenticated user." + } + ] + } + } +} diff --git a/lexicons/place/stream/vod/listDrafts.json b/lexicons/place/stream/vod/listDrafts.json new file mode 100644 index 000000000..e0c4458f3 --- /dev/null +++ b/lexicons/place/stream/vod/listDrafts.json @@ -0,0 +1,46 @@ +{ + "lexicon": 1, + "id": "place.stream.vod.listDrafts", + "defs": { + "main": { + "type": "query", + "description": "List the authenticated user's draft VODs, newest first.", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50, + "description": "Number of drafts to return." + }, + "cursor": { + "type": "string", + "description": "Pagination cursor from a previous response." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["drafts"], + "properties": { + "drafts": { + "type": "array", + "items": { + "type": "ref", + "ref": "place.stream.vod.draftDefs#draftView" + } + }, + "cursor": { + "type": "string", + "description": "Pagination cursor for the next page, if more results exist." + } + } + } + } + } + } +} diff --git a/lexicons/place/stream/vod/publishDraft.json b/lexicons/place/stream/vod/publishDraft.json new file mode 100644 index 000000000..0e85ea35e --- /dev/null +++ b/lexicons/place/stream/vod/publishDraft.json @@ -0,0 +1,52 @@ +{ + "lexicon": 1, + "id": "place.stream.vod.publishDraft", + "defs": { + "main": { + "type": "procedure", + "description": "Promote a ready draft VOD to a public place.stream.video record. The server carries over source and durationMs from the draft, backfills a generated thumbnail if the draft has none, writes the video record via putRecord, then deletes the draft.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "description": "The ats:// URI of the draft to publish. Must have status 'ready'." + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["videoUri", "videoCid"], + "properties": { + "videoUri": { + "type": "string", + "format": "at-uri", + "description": "AT-URI of the created place.stream.video record." + }, + "videoCid": { + "type": "string", + "format": "cid", + "description": "CID of the created place.stream.video record." + } + } + } + }, + "errors": [ + { + "name": "NotFound", + "description": "No draft exists with the given URI for the authenticated user." + }, + { + "name": "NotReady", + "description": "The draft is not ready to publish (status is 'processing' or 'error')." + } + ] + } + } +} diff --git a/lexicons/place/stream/vod/updateDraft.json b/lexicons/place/stream/vod/updateDraft.json new file mode 100644 index 000000000..5d0ab2d87 --- /dev/null +++ b/lexicons/place/stream/vod/updateDraft.json @@ -0,0 +1,88 @@ +{ + "lexicon": 1, + "id": "place.stream.vod.updateDraft", + "defs": { + "main": { + "type": "procedure", + "description": "Update a draft VOD's editable metadata. Server-authoritative fields (source, durationMs, status) are preserved. Editing is allowed in any status, so the user can pre-fill metadata while processing. All fields except uri are optional (partial update).", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "description": "The ats:// URI of the draft to update." + }, + "title": { + "type": "string", + "maxLength": 1400, + "maxGraphemes": 140 + }, + "description": { + "type": "string", + "maxLength": 50000, + "maxGraphemes": 5000 + }, + "descriptionFacets": { + "type": "array", + "items": { + "type": "ref", + "ref": "place.stream.richtext.videoFacet" + } + }, + "thumb": { + "type": "blob", + "accept": ["image/*"], + "maxSize": 1000000 + }, + "activity": { + "type": "union", + "refs": [ + "place.stream.defs#activityGame", + "place.stream.defs#activityLabel" + ] + }, + "tags": { + "type": "array", + "maxLength": 10, + "items": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + } + }, + "contentWarnings": { + "type": "ref", + "ref": "place.stream.metadata.contentWarnings" + }, + "contentRights": { + "type": "ref", + "ref": "place.stream.metadata.contentRights" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["draft"], + "properties": { + "draft": { + "type": "ref", + "ref": "place.stream.vod.draftDefs#draftView" + } + } + } + }, + "errors": [ + { + "name": "NotFound", + "description": "No draft exists with the given URI for the authenticated user." + } + ] + } + } +} diff --git a/pkg/gen/gen.go b/pkg/gen/gen.go index 508eea83c..3a5b25586 100644 --- a/pkg/gen/gen.go +++ b/pkg/gen/gen.go @@ -61,6 +61,7 @@ func main() { streamplace.VodComment_ReplyRef{}, streamplace.Like{}, streamplace.VodGate{}, + streamplace.VodDraftVideo{}, ); err != nil { panic(err) } diff --git a/pkg/spxrpc/place_stream_vod_drafts_stubs.go b/pkg/spxrpc/place_stream_vod_drafts_stubs.go new file mode 100644 index 000000000..f778146d0 --- /dev/null +++ b/pkg/spxrpc/place_stream_vod_drafts_stubs.go @@ -0,0 +1,36 @@ +package spxrpc + +import ( + "context" + "net/http" + + "github.com/labstack/echo/v4" + placestream "stream.place/streamplace/pkg/streamplace" +) + +// errNotImplemented is returned by the draft XRPC stubs until Phase 3 lands. +var errNotImplemented = echo.NewHTTPError(http.StatusNotImplemented, "draft VOD endpoint not yet implemented") + +// Stubs for the place.stream.vod.* draft XRPCs. Full implementations land in +// Phase 3 once the statedb draft storage layer exists; these satisfy the +// generated server stubs (stubs.go) so the tree compiles in the meantime. + +func (s *Server) handlePlaceStreamVodListDrafts(ctx context.Context, cursor string, limit int) (*placestream.VodListDrafts_Output, error) { + return nil, errNotImplemented +} + +func (s *Server) handlePlaceStreamVodGetDraft(ctx context.Context, uri string) (*placestream.VodGetDraft_Output, error) { + return nil, errNotImplemented +} + +func (s *Server) handlePlaceStreamVodUpdateDraft(ctx context.Context, body *placestream.VodUpdateDraft_Input) (*placestream.VodUpdateDraft_Output, error) { + return nil, errNotImplemented +} + +func (s *Server) handlePlaceStreamVodDeleteDraft(ctx context.Context, body *placestream.VodDeleteDraft_Input) error { + return errNotImplemented +} + +func (s *Server) handlePlaceStreamVodPublishDraft(ctx context.Context, body *placestream.VodPublishDraft_Input) (*placestream.VodPublishDraft_Output, error) { + return nil, errNotImplemented +} diff --git a/pkg/spxrpc/stubs.go b/pkg/spxrpc/stubs.go index 77c15f0fa..b6e08a697 100644 --- a/pkg/spxrpc/stubs.go +++ b/pkg/spxrpc/stubs.go @@ -338,7 +338,12 @@ func (s *Server) RegisterHandlersPlaceStream(e *echo.Echo) error { e.GET("/xrpc/place.stream.server.listWebhooks", s.HandlePlaceStreamServerListWebhooks) e.POST("/xrpc/place.stream.server.updateWebhook", s.HandlePlaceStreamServerUpdateWebhook) e.POST("/xrpc/place.stream.server.upsertStorage", s.HandlePlaceStreamServerUpsertStorage) + e.POST("/xrpc/place.stream.vod.deleteDraft", s.HandlePlaceStreamVodDeleteDraft) e.GET("/xrpc/place.stream.vod.getComments", s.HandlePlaceStreamVodGetComments) + e.GET("/xrpc/place.stream.vod.getDraft", s.HandlePlaceStreamVodGetDraft) + e.GET("/xrpc/place.stream.vod.listDrafts", s.HandlePlaceStreamVodListDrafts) + e.POST("/xrpc/place.stream.vod.publishDraft", s.HandlePlaceStreamVodPublishDraft) + e.POST("/xrpc/place.stream.vod.updateDraft", s.HandlePlaceStreamVodUpdateDraft) return nil } @@ -1351,6 +1356,23 @@ func (s *Server) HandlePlaceStreamServerUpsertStorage(c echo.Context) error { return c.JSON(200, out) } +func (s *Server) HandlePlaceStreamVodDeleteDraft(c echo.Context) error { + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamVodDeleteDraft") + defer span.End() + + var body placestream.VodDeleteDraft_Input + if err := c.Bind(&body); err != nil { + return err + } + var handleErr error + // func (s *Server) handlePlaceStreamVodDeleteDraft(ctx context.Context,body *placestream.VodDeleteDraft_Input) error + handleErr = s.handlePlaceStreamVodDeleteDraft(ctx, &body) + if handleErr != nil { + return handleErr + } + return nil +} + func (s *Server) HandlePlaceStreamVodGetComments(c echo.Context) error { ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamVodGetComments") defer span.End() @@ -1377,6 +1399,81 @@ func (s *Server) HandlePlaceStreamVodGetComments(c echo.Context) error { return c.JSON(200, out) } +func (s *Server) HandlePlaceStreamVodGetDraft(c echo.Context) error { + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamVodGetDraft") + defer span.End() + uri := c.QueryParam("uri") + var out *placestream.VodGetDraft_Output + var handleErr error + // func (s *Server) handlePlaceStreamVodGetDraft(ctx context.Context,uri string) (*placestream.VodGetDraft_Output, error) + out, handleErr = s.handlePlaceStreamVodGetDraft(ctx, uri) + if handleErr != nil { + return handleErr + } + return c.JSON(200, out) +} + +func (s *Server) HandlePlaceStreamVodListDrafts(c echo.Context) error { + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamVodListDrafts") + defer span.End() + cursor := c.QueryParam("cursor") + + var limit int + if p := c.QueryParam("limit"); p != "" { + var err error + limit, err = strconv.Atoi(p) + if err != nil { + return err + } + } else { + limit = 50 + } + var out *placestream.VodListDrafts_Output + var handleErr error + // func (s *Server) handlePlaceStreamVodListDrafts(ctx context.Context,cursor string,limit int) (*placestream.VodListDrafts_Output, error) + out, handleErr = s.handlePlaceStreamVodListDrafts(ctx, cursor, limit) + if handleErr != nil { + return handleErr + } + return c.JSON(200, out) +} + +func (s *Server) HandlePlaceStreamVodPublishDraft(c echo.Context) error { + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamVodPublishDraft") + defer span.End() + + var body placestream.VodPublishDraft_Input + if err := c.Bind(&body); err != nil { + return err + } + var out *placestream.VodPublishDraft_Output + var handleErr error + // func (s *Server) handlePlaceStreamVodPublishDraft(ctx context.Context,body *placestream.VodPublishDraft_Input) (*placestream.VodPublishDraft_Output, error) + out, handleErr = s.handlePlaceStreamVodPublishDraft(ctx, &body) + if handleErr != nil { + return handleErr + } + return c.JSON(200, out) +} + +func (s *Server) HandlePlaceStreamVodUpdateDraft(c echo.Context) error { + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamVodUpdateDraft") + defer span.End() + + var body placestream.VodUpdateDraft_Input + if err := c.Bind(&body); err != nil { + return err + } + var out *placestream.VodUpdateDraft_Output + var handleErr error + // func (s *Server) handlePlaceStreamVodUpdateDraft(ctx context.Context,body *placestream.VodUpdateDraft_Input) (*placestream.VodUpdateDraft_Output, error) + out, handleErr = s.handlePlaceStreamVodUpdateDraft(ctx, &body) + if handleErr != nil { + return handleErr + } + return c.JSON(200, out) +} + func (s *Server) RegisterHandlersToolsOzone(e *echo.Echo) error { return nil } diff --git a/pkg/streamplace/cbor_gen.go b/pkg/streamplace/cbor_gen.go index c9f37b356..27ed4122c 100644 --- a/pkg/streamplace/cbor_gen.go +++ b/pkg/streamplace/cbor_gen.go @@ -12409,3 +12409,847 @@ func (t *VodGate) UnmarshalCBOR(r io.Reader) (err error) { return nil } +func (t *VodDraftVideo) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 15 + + if t.Activity == nil { + fieldCount-- + } + + if t.Connections == nil { + fieldCount-- + } + + if t.ContentRights == nil { + fieldCount-- + } + + if t.ContentWarnings == nil { + fieldCount-- + } + + if t.Description == nil { + fieldCount-- + } + + if t.DescriptionFacets == nil { + fieldCount-- + } + + if t.DurationMs == nil { + fieldCount-- + } + + if t.Error == nil { + fieldCount-- + } + + if t.Source == nil { + fieldCount-- + } + + if t.Tags == nil { + fieldCount-- + } + + if t.Thumb == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Tags ([]string) (slice) + if t.Tags != nil { + + if len("tags") > 1000000 { + return xerrors.Errorf("Value in field \"tags\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("tags"))); err != nil { + return err + } + if _, err := cw.WriteString(string("tags")); err != nil { + return err + } + + if len(t.Tags) > 8192 { + return xerrors.Errorf("Slice value in field t.Tags was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Tags))); err != nil { + return err + } + for _, v := range t.Tags { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("place.stream.vod.draftVideo"))); err != nil { + return err + } + if _, err := cw.WriteString(string("place.stream.vod.draftVideo")); err != nil { + return err + } + + // t.Error (string) (string) + if t.Error != nil { + + if len("error") > 1000000 { + return xerrors.Errorf("Value in field \"error\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("error"))); err != nil { + return err + } + if _, err := cw.WriteString(string("error")); err != nil { + return err + } + + if t.Error == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Error) > 1000000 { + return xerrors.Errorf("Value in field t.Error was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Error))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Error)); err != nil { + return err + } + } + } + + // t.Thumb (util.LexBlob) (struct) + if t.Thumb != nil { + + if len("thumb") > 1000000 { + return xerrors.Errorf("Value in field \"thumb\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("thumb"))); err != nil { + return err + } + if _, err := cw.WriteString(string("thumb")); err != nil { + return err + } + + if err := t.Thumb.MarshalCBOR(cw); err != nil { + return err + } + } + + // t.Title (string) (string) + if len("title") > 1000000 { + return xerrors.Errorf("Value in field \"title\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("title"))); err != nil { + return err + } + if _, err := cw.WriteString(string("title")); err != nil { + return err + } + + if len(t.Title) > 1000000 { + return xerrors.Errorf("Value in field t.Title was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Title))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Title)); err != nil { + return err + } + + // t.Source (streamplace.VodDraftVideo_Source) (struct) + if t.Source != nil { + + if len("source") > 1000000 { + return xerrors.Errorf("Value in field \"source\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("source"))); err != nil { + return err + } + if _, err := cw.WriteString(string("source")); err != nil { + return err + } + + if err := t.Source.MarshalCBOR(cw); err != nil { + return err + } + } + + // t.Status (string) (string) + if len("status") > 1000000 { + return xerrors.Errorf("Value in field \"status\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("status"))); err != nil { + return err + } + if _, err := cw.WriteString(string("status")); err != nil { + return err + } + + if len(t.Status) > 1000000 { + return xerrors.Errorf("Value in field t.Status was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Status))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Status)); err != nil { + return err + } + + // t.Activity (streamplace.VodDraftVideo_Activity) (struct) + if t.Activity != nil { + + if len("activity") > 1000000 { + return xerrors.Errorf("Value in field \"activity\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("activity"))); err != nil { + return err + } + if _, err := cw.WriteString(string("activity")); err != nil { + return err + } + + if err := t.Activity.MarshalCBOR(cw); err != nil { + return err + } + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + + // t.DurationMs (int64) (int64) + if t.DurationMs != nil { + + if len("durationMs") > 1000000 { + return xerrors.Errorf("Value in field \"durationMs\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("durationMs"))); err != nil { + return err + } + if _, err := cw.WriteString(string("durationMs")); err != nil { + return err + } + + if t.DurationMs == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if *t.DurationMs >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.DurationMs)); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.DurationMs-1)); err != nil { + return err + } + } + } + + } + + // t.Connections ([]*streamplace.VodDraftVideo_Connections_Elem) (slice) + if t.Connections != nil { + + if len("connections") > 1000000 { + return xerrors.Errorf("Value in field \"connections\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("connections"))); err != nil { + return err + } + if _, err := cw.WriteString(string("connections")); err != nil { + return err + } + + if len(t.Connections) > 8192 { + return xerrors.Errorf("Slice value in field t.Connections was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Connections))); err != nil { + return err + } + for _, v := range t.Connections { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + } + + // t.Description (string) (string) + if t.Description != nil { + + if len("description") > 1000000 { + return xerrors.Errorf("Value in field \"description\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("description"))); err != nil { + return err + } + if _, err := cw.WriteString(string("description")); err != nil { + return err + } + + if t.Description == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Description) > 1000000 { + return xerrors.Errorf("Value in field t.Description was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Description))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Description)); err != nil { + return err + } + } + } + + // t.ContentRights (streamplace.MetadataContentRights) (struct) + if t.ContentRights != nil { + + if len("contentRights") > 1000000 { + return xerrors.Errorf("Value in field \"contentRights\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("contentRights"))); err != nil { + return err + } + if _, err := cw.WriteString(string("contentRights")); err != nil { + return err + } + + if err := t.ContentRights.MarshalCBOR(cw); err != nil { + return err + } + } + + // t.ContentWarnings (streamplace.MetadataContentWarnings) (struct) + if t.ContentWarnings != nil { + + if len("contentWarnings") > 1000000 { + return xerrors.Errorf("Value in field \"contentWarnings\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("contentWarnings"))); err != nil { + return err + } + if _, err := cw.WriteString(string("contentWarnings")); err != nil { + return err + } + + if err := t.ContentWarnings.MarshalCBOR(cw); err != nil { + return err + } + } + + // t.DescriptionFacets ([]*streamplace.RichtextVideoFacet) (slice) + if t.DescriptionFacets != nil { + + if len("descriptionFacets") > 1000000 { + return xerrors.Errorf("Value in field \"descriptionFacets\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("descriptionFacets"))); err != nil { + return err + } + if _, err := cw.WriteString(string("descriptionFacets")); err != nil { + return err + } + + if len(t.DescriptionFacets) > 8192 { + return xerrors.Errorf("Slice value in field t.DescriptionFacets was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.DescriptionFacets))); err != nil { + return err + } + for _, v := range t.DescriptionFacets { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + } + return nil +} + +func (t *VodDraftVideo) UnmarshalCBOR(r io.Reader) (err error) { + *t = VodDraftVideo{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("VodDraftVideo: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 17) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Tags ([]string) (slice) + case "tags": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Tags: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Tags = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Tags[i] = string(sval) + } + + } + } + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Error (string) (string) + case "error": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Error = (*string)(&sval) + } + } + // t.Thumb (util.LexBlob) (struct) + case "thumb": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Thumb = new(util.LexBlob) + if err := t.Thumb.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Thumb pointer: %w", err) + } + } + + } + // t.Title (string) (string) + case "title": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Title = string(sval) + } + // t.Source (streamplace.VodDraftVideo_Source) (struct) + case "source": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Source = new(VodDraftVideo_Source) + if err := t.Source.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Source pointer: %w", err) + } + } + + } + // t.Status (string) (string) + case "status": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Status = string(sval) + } + // t.Activity (streamplace.VodDraftVideo_Activity) (struct) + case "activity": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Activity = new(VodDraftVideo_Activity) + if err := t.Activity.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Activity pointer: %w", err) + } + } + + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + // t.DurationMs (int64) (int64) + case "durationMs": + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + var extraI int64 + switch maj { + case cbg.MajUnsignedInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 positive overflow") + } + case cbg.MajNegativeInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 negative overflow") + } + extraI = -1 - extraI + default: + return fmt.Errorf("wrong type for int64 field: %d", maj) + } + + t.DurationMs = (*int64)(&extraI) + } + } + // t.Connections ([]*streamplace.VodDraftVideo_Connections_Elem) (slice) + case "connections": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Connections: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Connections = make([]*VodDraftVideo_Connections_Elem, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Connections[i] = new(VodDraftVideo_Connections_Elem) + if err := t.Connections[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Connections[i] pointer: %w", err) + } + } + + } + + } + } + // t.Description (string) (string) + case "description": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Description = (*string)(&sval) + } + } + // t.ContentRights (streamplace.MetadataContentRights) (struct) + case "contentRights": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.ContentRights = new(MetadataContentRights) + if err := t.ContentRights.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.ContentRights pointer: %w", err) + } + } + + } + // t.ContentWarnings (streamplace.MetadataContentWarnings) (struct) + case "contentWarnings": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.ContentWarnings = new(MetadataContentWarnings) + if err := t.ContentWarnings.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.ContentWarnings pointer: %w", err) + } + } + + } + // t.DescriptionFacets ([]*streamplace.RichtextVideoFacet) (slice) + case "descriptionFacets": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.DescriptionFacets: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.DescriptionFacets = make([]*RichtextVideoFacet, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.DescriptionFacets[i] = new(RichtextVideoFacet) + if err := t.DescriptionFacets[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.DescriptionFacets[i] pointer: %w", err) + } + } + + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/pkg/streamplace/mediafinalizeLivestream.go b/pkg/streamplace/mediafinalizeLivestream.go index bf451c9c5..28c3f221d 100644 --- a/pkg/streamplace/mediafinalizeLivestream.go +++ b/pkg/streamplace/mediafinalizeLivestream.go @@ -18,7 +18,9 @@ type MediaFinalizeLivestream_Input struct { // MediaFinalizeLivestream_Output is the output of a place.stream.media.finalizeLivestream call. type MediaFinalizeLivestream_Output struct { - // uploadId: Identifier for the finalize job. Poll place.stream.media.getUploadStatus with it; once status is 'done', create the video with place.stream.media.publishVideo. + // draftUri: The ats:// URI of the draft VOD created for this finalize. The draft reaches status 'ready' when processing completes; the user publishes it from the Drafts tab via place.stream.vod.publishDraft. + DraftUri string `json:"draftUri" cborgen:"draftUri"` + // uploadId: Identifier for the finalize job. Retained for backwards compatibility; the draft flow no longer requires the client to poll getUploadStatus. UploadId string `json:"uploadId" cborgen:"uploadId"` } diff --git a/pkg/streamplace/voddeleteDraft.go b/pkg/streamplace/voddeleteDraft.go new file mode 100644 index 000000000..c70b092dd --- /dev/null +++ b/pkg/streamplace/voddeleteDraft.go @@ -0,0 +1,26 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: place.stream.vod.deleteDraft + +package streamplace + +import ( + "context" + + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +// VodDeleteDraft_Input is the input argument to a place.stream.vod.deleteDraft call. +type VodDeleteDraft_Input struct { + // uri: The ats:// URI of the draft to discard. + Uri string `json:"uri" cborgen:"uri"` +} + +// VodDeleteDraft calls the XRPC method "place.stream.vod.deleteDraft". +func VodDeleteDraft(ctx context.Context, c lexutil.LexClient, input *VodDeleteDraft_Input) error { + if err := c.LexDo(ctx, lexutil.Procedure, "application/json", "place.stream.vod.deleteDraft", nil, input, nil); err != nil { + return err + } + + return nil +} diff --git a/pkg/streamplace/voddraftDefs.go b/pkg/streamplace/voddraftDefs.go new file mode 100644 index 000000000..2dc5e3300 --- /dev/null +++ b/pkg/streamplace/voddraftDefs.go @@ -0,0 +1,16 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: place.stream.vod.draftDefs + +package streamplace + +// VodDraftDefs_DraftView is a "draftView" in the place.stream.vod.draftDefs schema. +// +// A draft VOD with its ats:// URI and CID, for list/get responses. +type VodDraftDefs_DraftView struct { + // cid: CID (sha256 of CBOR record bytes, base32) of the draft record. + Cid string `json:"cid" cborgen:"cid"` + Record *VodDraftVideo `json:"record" cborgen:"record"` + // uri: The ats:// URI of the draft record. + Uri string `json:"uri" cborgen:"uri"` +} diff --git a/pkg/streamplace/voddraftVideo.go b/pkg/streamplace/voddraftVideo.go new file mode 100644 index 000000000..535c4ddaa --- /dev/null +++ b/pkg/streamplace/voddraftVideo.go @@ -0,0 +1,240 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: place.stream.vod.draftVideo + +package streamplace + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + + lexutil "github.com/bluesky-social/indigo/lex/util" + cbg "github.com/whyrusleeping/cbor-gen" +) + +func init() { + lexutil.RegisterType("place.stream.vod.draftVideo", &VodDraftVideo{}) +} + +type VodDraftVideo struct { + LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.vod.draftVideo"` + // activity: The game or activity in the video. + Activity *VodDraftVideo_Activity `json:"activity,omitempty" cborgen:"activity,omitempty"` + // connections: Related records (e.g. the source livestream for a finalized VOD). + Connections []*VodDraftVideo_Connections_Elem `json:"connections,omitempty" cborgen:"connections,omitempty"` + ContentRights *MetadataContentRights `json:"contentRights,omitempty" cborgen:"contentRights,omitempty"` + ContentWarnings *MetadataContentWarnings `json:"contentWarnings,omitempty" cborgen:"contentWarnings,omitempty"` + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + // description: Description of the video. + Description *string `json:"description,omitempty" cborgen:"description,omitempty"` + // descriptionFacets: Annotations of text (mentions, URLs, etc). + DescriptionFacets []*RichtextVideoFacet `json:"descriptionFacets,omitempty" cborgen:"descriptionFacets,omitempty"` + // durationMs: Duration in milliseconds. Filled when status is 'ready'. + DurationMs *int64 `json:"durationMs,omitempty" cborgen:"durationMs,omitempty"` + // error: Error message when status is 'error'. + Error *string `json:"error,omitempty" cborgen:"error,omitempty"` + // source: Source tracks. Filled by the server when processing completes (status 'ready'). + Source *VodDraftVideo_Source `json:"source,omitempty" cborgen:"source,omitempty"` + // status: Processing state: 'processing' while the job runs, 'ready' when publishable, 'error' if processing failed. + Status string `json:"status" cborgen:"status"` + // tags: Freeform tags (same rules as place.stream.video). + Tags []string `json:"tags,omitempty" cborgen:"tags,omitempty"` + // thumb: Thumbnail image. Generated by the server at publish time if absent. + Thumb *lexutil.LexBlob `json:"thumb,omitempty" cborgen:"thumb,omitempty"` + // title: Title of the video. May be a placeholder while processing; the user edits before publishing. + Title string `json:"title" cborgen:"title"` +} + +// The game or activity in the video. +type VodDraftVideo_Activity struct { + Defs_ActivityGame *Defs_ActivityGame + Defs_ActivityLabel *Defs_ActivityLabel +} + +func (t *VodDraftVideo_Activity) MarshalJSON() ([]byte, error) { + if t.Defs_ActivityGame != nil { + t.Defs_ActivityGame.LexiconTypeID = "place.stream.defs#activityGame" + return json.Marshal(t.Defs_ActivityGame) + } + if t.Defs_ActivityLabel != nil { + t.Defs_ActivityLabel.LexiconTypeID = "place.stream.defs#activityLabel" + return json.Marshal(t.Defs_ActivityLabel) + } + return nil, fmt.Errorf("can not marshal empty union as JSON") +} + +func (t *VodDraftVideo_Activity) UnmarshalJSON(b []byte) error { + typ, err := lexutil.TypeExtract(b) + if err != nil { + return err + } + + switch typ { + case "place.stream.defs#activityGame": + t.Defs_ActivityGame = new(Defs_ActivityGame) + return json.Unmarshal(b, t.Defs_ActivityGame) + case "place.stream.defs#activityLabel": + t.Defs_ActivityLabel = new(Defs_ActivityLabel) + return json.Unmarshal(b, t.Defs_ActivityLabel) + default: + return nil + } +} + +func (t *VodDraftVideo_Activity) MarshalCBOR(w io.Writer) error { + + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + if t.Defs_ActivityGame != nil { + return t.Defs_ActivityGame.MarshalCBOR(w) + } + if t.Defs_ActivityLabel != nil { + return t.Defs_ActivityLabel.MarshalCBOR(w) + } + return fmt.Errorf("can not marshal empty union as CBOR") +} + +func (t *VodDraftVideo_Activity) UnmarshalCBOR(r io.Reader) error { + typ, b, err := lexutil.CborTypeExtractReader(r) + if err != nil { + return err + } + + switch typ { + case "place.stream.defs#activityGame": + t.Defs_ActivityGame = new(Defs_ActivityGame) + return t.Defs_ActivityGame.UnmarshalCBOR(bytes.NewReader(b)) + case "place.stream.defs#activityLabel": + t.Defs_ActivityLabel = new(Defs_ActivityLabel) + return t.Defs_ActivityLabel.UnmarshalCBOR(bytes.NewReader(b)) + default: + return nil + } +} + +type VodDraftVideo_Connections_Elem struct { + Video_Connection *Video_Connection +} + +func (t *VodDraftVideo_Connections_Elem) MarshalJSON() ([]byte, error) { + if t.Video_Connection != nil { + t.Video_Connection.LexiconTypeID = "place.stream.video#connection" + return json.Marshal(t.Video_Connection) + } + return nil, fmt.Errorf("can not marshal empty union as JSON") +} + +func (t *VodDraftVideo_Connections_Elem) UnmarshalJSON(b []byte) error { + typ, err := lexutil.TypeExtract(b) + if err != nil { + return err + } + + switch typ { + case "place.stream.video#connection": + t.Video_Connection = new(Video_Connection) + return json.Unmarshal(b, t.Video_Connection) + default: + return nil + } +} + +func (t *VodDraftVideo_Connections_Elem) MarshalCBOR(w io.Writer) error { + + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + if t.Video_Connection != nil { + return t.Video_Connection.MarshalCBOR(w) + } + return fmt.Errorf("can not marshal empty union as CBOR") +} + +func (t *VodDraftVideo_Connections_Elem) UnmarshalCBOR(r io.Reader) error { + typ, b, err := lexutil.CborTypeExtractReader(r) + if err != nil { + return err + } + + switch typ { + case "place.stream.video#connection": + t.Video_Connection = new(Video_Connection) + return t.Video_Connection.UnmarshalCBOR(bytes.NewReader(b)) + default: + return nil + } +} + +// Source tracks. Filled by the server when processing completes (status 'ready'). +type VodDraftVideo_Source struct { + MediaDefs_SourceTracks *MediaDefs_SourceTracks + MediaDefs_SourceClip *MediaDefs_SourceClip +} + +func (t *VodDraftVideo_Source) MarshalJSON() ([]byte, error) { + if t.MediaDefs_SourceTracks != nil { + t.MediaDefs_SourceTracks.LexiconTypeID = "place.stream.media.defs#sourceTracks" + return json.Marshal(t.MediaDefs_SourceTracks) + } + if t.MediaDefs_SourceClip != nil { + t.MediaDefs_SourceClip.LexiconTypeID = "place.stream.media.defs#sourceClip" + return json.Marshal(t.MediaDefs_SourceClip) + } + return nil, fmt.Errorf("can not marshal empty union as JSON") +} + +func (t *VodDraftVideo_Source) UnmarshalJSON(b []byte) error { + typ, err := lexutil.TypeExtract(b) + if err != nil { + return err + } + + switch typ { + case "place.stream.media.defs#sourceTracks": + t.MediaDefs_SourceTracks = new(MediaDefs_SourceTracks) + return json.Unmarshal(b, t.MediaDefs_SourceTracks) + case "place.stream.media.defs#sourceClip": + t.MediaDefs_SourceClip = new(MediaDefs_SourceClip) + return json.Unmarshal(b, t.MediaDefs_SourceClip) + default: + return nil + } +} + +func (t *VodDraftVideo_Source) MarshalCBOR(w io.Writer) error { + + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + if t.MediaDefs_SourceTracks != nil { + return t.MediaDefs_SourceTracks.MarshalCBOR(w) + } + if t.MediaDefs_SourceClip != nil { + return t.MediaDefs_SourceClip.MarshalCBOR(w) + } + return fmt.Errorf("can not marshal empty union as CBOR") +} + +func (t *VodDraftVideo_Source) UnmarshalCBOR(r io.Reader) error { + typ, b, err := lexutil.CborTypeExtractReader(r) + if err != nil { + return err + } + + switch typ { + case "place.stream.media.defs#sourceTracks": + t.MediaDefs_SourceTracks = new(MediaDefs_SourceTracks) + return t.MediaDefs_SourceTracks.UnmarshalCBOR(bytes.NewReader(b)) + case "place.stream.media.defs#sourceClip": + t.MediaDefs_SourceClip = new(MediaDefs_SourceClip) + return t.MediaDefs_SourceClip.UnmarshalCBOR(bytes.NewReader(b)) + default: + return nil + } +} diff --git a/pkg/streamplace/vodgetDraft.go b/pkg/streamplace/vodgetDraft.go new file mode 100644 index 000000000..f2b3fa29b --- /dev/null +++ b/pkg/streamplace/vodgetDraft.go @@ -0,0 +1,31 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: place.stream.vod.getDraft + +package streamplace + +import ( + "context" + + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +// VodGetDraft_Output is the output of a place.stream.vod.getDraft call. +type VodGetDraft_Output struct { + Draft *VodDraftDefs_DraftView `json:"draft" cborgen:"draft"` +} + +// VodGetDraft calls the XRPC method "place.stream.vod.getDraft". +// +// uri: The ats:// URI of the draft record. +func VodGetDraft(ctx context.Context, c lexutil.LexClient, uri string) (*VodGetDraft_Output, error) { + var out VodGetDraft_Output + + params := map[string]interface{}{} + params["uri"] = uri + if err := c.LexDo(ctx, lexutil.Query, "", "place.stream.vod.getDraft", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/pkg/streamplace/vodlistDrafts.go b/pkg/streamplace/vodlistDrafts.go new file mode 100644 index 000000000..5690af72f --- /dev/null +++ b/pkg/streamplace/vodlistDrafts.go @@ -0,0 +1,39 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: place.stream.vod.listDrafts + +package streamplace + +import ( + "context" + + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +// VodListDrafts_Output is the output of a place.stream.vod.listDrafts call. +type VodListDrafts_Output struct { + // cursor: Pagination cursor for the next page, if more results exist. + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` + Drafts []*VodDraftDefs_DraftView `json:"drafts" cborgen:"drafts"` +} + +// VodListDrafts calls the XRPC method "place.stream.vod.listDrafts". +// +// cursor: Pagination cursor from a previous response. +// limit: Number of drafts to return. +func VodListDrafts(ctx context.Context, c lexutil.LexClient, cursor string, limit int64) (*VodListDrafts_Output, error) { + var out VodListDrafts_Output + + params := map[string]interface{}{} + if cursor != "" { + params["cursor"] = cursor + } + if limit != 0 { + params["limit"] = limit + } + if err := c.LexDo(ctx, lexutil.Query, "", "place.stream.vod.listDrafts", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/pkg/streamplace/vodpublishDraft.go b/pkg/streamplace/vodpublishDraft.go new file mode 100644 index 000000000..799da5a18 --- /dev/null +++ b/pkg/streamplace/vodpublishDraft.go @@ -0,0 +1,35 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: place.stream.vod.publishDraft + +package streamplace + +import ( + "context" + + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +// VodPublishDraft_Input is the input argument to a place.stream.vod.publishDraft call. +type VodPublishDraft_Input struct { + // uri: The ats:// URI of the draft to publish. Must have status 'ready'. + Uri string `json:"uri" cborgen:"uri"` +} + +// VodPublishDraft_Output is the output of a place.stream.vod.publishDraft call. +type VodPublishDraft_Output struct { + // videoCid: CID of the created place.stream.video record. + VideoCid string `json:"videoCid" cborgen:"videoCid"` + // videoUri: AT-URI of the created place.stream.video record. + VideoUri string `json:"videoUri" cborgen:"videoUri"` +} + +// VodPublishDraft calls the XRPC method "place.stream.vod.publishDraft". +func VodPublishDraft(ctx context.Context, c lexutil.LexClient, input *VodPublishDraft_Input) (*VodPublishDraft_Output, error) { + var out VodPublishDraft_Output + if err := c.LexDo(ctx, lexutil.Procedure, "application/json", "place.stream.vod.publishDraft", nil, input, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/pkg/streamplace/vodupdateDraft.go b/pkg/streamplace/vodupdateDraft.go new file mode 100644 index 000000000..6cbfcb7d2 --- /dev/null +++ b/pkg/streamplace/vodupdateDraft.go @@ -0,0 +1,77 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: place.stream.vod.updateDraft + +package streamplace + +import ( + "context" + "encoding/json" + "fmt" + + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +// VodUpdateDraft_Input is the input argument to a place.stream.vod.updateDraft call. +type VodUpdateDraft_Input struct { + Activity *VodUpdateDraft_Input_Activity `json:"activity,omitempty" cborgen:"activity,omitempty"` + ContentRights *MetadataContentRights `json:"contentRights,omitempty" cborgen:"contentRights,omitempty"` + ContentWarnings *MetadataContentWarnings `json:"contentWarnings,omitempty" cborgen:"contentWarnings,omitempty"` + Description *string `json:"description,omitempty" cborgen:"description,omitempty"` + DescriptionFacets []*RichtextVideoFacet `json:"descriptionFacets,omitempty" cborgen:"descriptionFacets,omitempty"` + Tags []string `json:"tags,omitempty" cborgen:"tags,omitempty"` + Thumb *lexutil.LexBlob `json:"thumb,omitempty" cborgen:"thumb,omitempty"` + Title *string `json:"title,omitempty" cborgen:"title,omitempty"` + // uri: The ats:// URI of the draft to update. + Uri string `json:"uri" cborgen:"uri"` +} + +type VodUpdateDraft_Input_Activity struct { + Defs_ActivityGame *Defs_ActivityGame + Defs_ActivityLabel *Defs_ActivityLabel +} + +func (t *VodUpdateDraft_Input_Activity) MarshalJSON() ([]byte, error) { + if t.Defs_ActivityGame != nil { + t.Defs_ActivityGame.LexiconTypeID = "place.stream.defs#activityGame" + return json.Marshal(t.Defs_ActivityGame) + } + if t.Defs_ActivityLabel != nil { + t.Defs_ActivityLabel.LexiconTypeID = "place.stream.defs#activityLabel" + return json.Marshal(t.Defs_ActivityLabel) + } + return nil, fmt.Errorf("can not marshal empty union as JSON") +} + +func (t *VodUpdateDraft_Input_Activity) UnmarshalJSON(b []byte) error { + typ, err := lexutil.TypeExtract(b) + if err != nil { + return err + } + + switch typ { + case "place.stream.defs#activityGame": + t.Defs_ActivityGame = new(Defs_ActivityGame) + return json.Unmarshal(b, t.Defs_ActivityGame) + case "place.stream.defs#activityLabel": + t.Defs_ActivityLabel = new(Defs_ActivityLabel) + return json.Unmarshal(b, t.Defs_ActivityLabel) + default: + return nil + } +} + +// VodUpdateDraft_Output is the output of a place.stream.vod.updateDraft call. +type VodUpdateDraft_Output struct { + Draft *VodDraftDefs_DraftView `json:"draft" cborgen:"draft"` +} + +// VodUpdateDraft calls the XRPC method "place.stream.vod.updateDraft". +func VodUpdateDraft(ctx context.Context, c lexutil.LexClient, input *VodUpdateDraft_Input) (*VodUpdateDraft_Output, error) { + var out VodUpdateDraft_Output + if err := c.LexDo(ctx, lexutil.Procedure, "application/json", "place.stream.vod.updateDraft", nil, input, &out); err != nil { + return nil, err + } + + return &out, nil +}