Initial commit: YouTube Studio Flow (backend, frontend, infrastructure, docs)

This commit is contained in:
2026-08-11 12:27:44 +02:00
commit d5af006443
304 changed files with 74604 additions and 0 deletions
@@ -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[]`