161 lines
4.9 KiB
Markdown
161 lines
4.9 KiB
Markdown
# 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[]`
|