Initial commit: YouTube Studio Flow (backend, frontend, infrastructure, docs)
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
# Videos API
|
||||
|
||||
Base path: `/api/v1/videos`
|
||||
|
||||
All endpoints require `Authorization: Bearer <jwt>`.
|
||||
|
||||
Related: [[01 - Video Management]], [[04 - Database Schema]], [[11 - Bulk Jobs API]], [[07 - Playlists API]]
|
||||
|
||||
---
|
||||
|
||||
## GET /videos
|
||||
|
||||
List videos for the current team with pagination, sorting, and filtering.
|
||||
|
||||
**Query parameters:**
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `page` | number | Page number (default: `1`) |
|
||||
| `pageSize` | number | Items per page (default: `20`) |
|
||||
| `sortBy` | string | Field to sort by |
|
||||
| `sortOrder` | `asc\|desc` | Sort direction |
|
||||
| `search` | string | Title search (case-insensitive) |
|
||||
| `lintStatus` | `OK\|WARNING\|ERROR` | Exact lint status match |
|
||||
| `hasLintIssues` | boolean | Match `ERROR` or `WARNING` |
|
||||
| `privacyStatus` | `PUBLIC\|PRIVATE\|UNLISTED` | Privacy filter |
|
||||
| `scheduled` | boolean | Videos with a future `scheduledAt` |
|
||||
| `notScheduled` | boolean | Videos without a `scheduledAt` value |
|
||||
| `remoteConflict` | boolean | Videos with a remote conflict flag |
|
||||
| `pendingSync` | boolean | Videos where `hasPendingChanges` is `true` |
|
||||
| `deletedOnYouTube` | boolean | Videos with `youtubeDeletedAt` set |
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [ "VideoSummary[]" ],
|
||||
"total": 142,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
```
|
||||
|
||||
`hasPendingChanges` is computed per video by comparing the current state hash against `lastSyncedHash`. See [[01 - System Overview]] for details on the sync status logic.
|
||||
|
||||
---
|
||||
|
||||
## GET /videos/:id
|
||||
|
||||
Get full video detail including lint results, playlists, and collaborators.
|
||||
|
||||
**Response:** `VideoDetail` object
|
||||
|
||||
---
|
||||
|
||||
## PATCH /videos/:id
|
||||
|
||||
Update video metadata fields.
|
||||
|
||||
**Request body** (all fields optional):
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "string",
|
||||
"tags": ["string"],
|
||||
"privacyStatus": "PUBLIC|PRIVATE|UNLISTED",
|
||||
"scheduledAt": "ISO datetime",
|
||||
"categoryId": "string",
|
||||
"selfDeclaredMadeForKids": false,
|
||||
"embeddable": true,
|
||||
"license": "youtube|creativeCommon",
|
||||
"defaultLanguage": "en",
|
||||
"defaultAudioLanguage": "en",
|
||||
"recordingDate": "YYYY-MM-DD",
|
||||
"gameTitle": "string",
|
||||
"collaboratorIds": ["cuid"],
|
||||
"templateId": "cuid"
|
||||
}
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- `recordingDate` is not returned by YouTube during import and is always `null` after a channel sync. Setting it locally will correctly mark the video as push-pending.
|
||||
- Providing `collaboratorIds` updates `Video.collaboratorIds` (a JSON string array on the `Video` row). This is the single field the render engine reads to resolve `{collab.*}` tokens.
|
||||
- Providing `templateId` applies the template's block configuration to the video's `VideoConfig`.
|
||||
|
||||
---
|
||||
|
||||
## POST /videos/:id/sync
|
||||
|
||||
Enqueue a YouTube sync job for the video. Returns immediately; processing is asynchronous via BullMQ.
|
||||
|
||||
The processor renders the description, computes a metadata hash, skips if unchanged, then pushes all editable fields to YouTube. After a successful push, `renderedDescription` and `lastSyncedHash` are updated on the video row.
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{ "queued": true }
|
||||
```
|
||||
|
||||
**Cost:** 50 YouTube API quota units per successful push. See [[10 - Quota API]].
|
||||
|
||||
---
|
||||
|
||||
## POST /videos/:id/refresh
|
||||
|
||||
Pull latest metadata from YouTube and update the local record. This is a synchronous operation.
|
||||
|
||||
> Re-importing after making local changes will reset the `lastSyncedHash` baseline.
|
||||
|
||||
**Response:** Updated `VideoDetail`
|
||||
|
||||
---
|
||||
|
||||
## POST /videos/:id/accept-remote
|
||||
|
||||
Resolve a detected remote conflict by adopting the pending remote snapshot as the new local state. Requires **EDITOR** role. **Zero YouTube API calls** — operates entirely on the snapshot captured by `detectConflict()` when the conflict was first detected.
|
||||
|
||||
**Behavior:**
|
||||
1. Copies `pendingRemoteSnapshot` fields into the live Video columns (`title`, `tags`, `categoryId`, `privacyStatus`, `defaultLanguage`, `defaultAudioLanguage`, `selfDeclaredMadeForKids`, `embeddable`, `license`, `recordingDate`)
|
||||
2. Sets `youtubeDescription = renderedDescription = pendingRemoteDescription`
|
||||
3. Promotes `pendingRemoteSnapshot` to `youtubeSnapshot`
|
||||
4. Recomputes `lastSyncedHash` from the accepted state, sets `lastSyncedAt = now`
|
||||
5. Clears `pendingRemoteSnapshot`, `pendingRemoteDescription`, `remoteConflict`
|
||||
6. Writes an audit log entry with `action: 'accept-remote'`
|
||||
|
||||
**Errors:**
|
||||
- `400` if there is no `pendingRemoteSnapshot` to accept
|
||||
- `404` if the video is not in the caller's team
|
||||
|
||||
**Response:** Updated `Video` row.
|
||||
|
||||
For the opposite direction ("keep local, overwrite remote"), just call `POST /videos/:id/sync` — the sync processor overwrites remote and clears `remoteConflict` on success.
|
||||
|
||||
---
|
||||
|
||||
## POST /videos/:id/render
|
||||
|
||||
Render the video's description using the current `VideoConfig` and return the result without saving or pushing.
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{ "rendered": "string" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DELETE /videos/:id
|
||||
|
||||
Soft-delete the video (marks as deleted locally). Does not delete the video from YouTube.
|
||||
|
||||
---
|
||||
|
||||
## GET /youtube-sync/channels
|
||||
|
||||
List connected YouTube channels for the current team.
|
||||
|
||||
Base path: `/api/v1/youtube-sync/channels`
|
||||
|
||||
**Response:** `Channel[]`
|
||||
Reference in New Issue
Block a user