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
+}