TheOrb/WATCHPARTY_IMPLEMENTATION_PLAN.md

240 lines
4.9 KiB
Markdown

# Jellyfin VC Watch Party Plan
## Goal
Add Discord voice-channel watch parties to The Orb Bot where users choose media from the Jellyfin-backed catalog and a separate streaming worker pushes the selected video into a Discord VC.
The existing Python bot remains the control plane. The streaming runtime is a separate worker service.
## Why This Split Exists
The current bot is responsible for:
- dashboard and API
- Jellyfin library sync
- persistent state in SQLite
- Discord message/status publishing
It is not a video-streaming runtime. Discord VC video streaming needs a dedicated worker process with:
- Discord streaming session handling
- FFmpeg process control
- queue playback
- reconnect and health reporting
## Target Architecture
### 1. Orb Bot (`archive_bot`)
Responsibilities:
- create/manage watch-party sessions
- browse Jellyfin media
- queue items
- persist session state
- expose API/dashboard controls
- send commands to the stream worker
- read worker state
### 2. Stream Worker (`orb-stream-worker`)
Responsibilities:
- join a Discord VC
- start and stop the stream
- pull the selected item from Jellyfin
- transcode or normalize media as needed
- report current playback state
- handle pause/resume/seek/skip
### 3. Jellyfin
Responsibilities:
- source media metadata
- source playback URLs/files
- optional transcoding source if needed
## Current Foundation Implemented In This Repo
This repo now contains the control-plane foundation:
- SQLite-backed watch-party tables
- session, queue, event, and worker-state models
- HTTP APIs for watch-party orchestration
The stream worker itself is intentionally not implemented inside the Python bot.
## Data Model
### `watch_party_sessions`
- `id`
- `guild_id`
- `voice_channel_id`
- `text_channel_id`
- `owner_user_id`
- `title`
- `status` (`draft`, `queued`, `connecting`, `playing`, `paused`, `stopped`, `error`)
- `worker_session_id`
- `current_queue_entry_id`
- `created_at`
- `updated_at`
### `watch_party_queue`
- `id`
- `session_id`
- `position`
- `media_type`
- `jellyfin_source_id`
- `title`
- `year`
- `runtime`
- `summary`
- `poster_url`
- `status` (`queued`, `playing`, `played`, `skipped`, `failed`)
- `created_at`
### `watch_party_events`
- `id`
- `session_id`
- `event_type`
- `payload_json`
- `created_at`
### `watch_party_worker_state`
- `session_id`
- `worker_status`
- `playback_state`
- `current_title`
- `position_seconds`
- `duration_seconds`
- `last_error`
- `updated_at`
## Worker Contract
The Python bot should speak to the worker through a private HTTP API on the Docker network.
### `POST /sessions`
Create/connect a worker session.
Request:
```json
{
"sessionId": "local-session-id",
"guildId": "123",
"voiceChannelId": "456",
"textChannelId": "789"
}
```
### `POST /sessions/{sessionId}/play`
Start one Jellyfin item.
Request:
```json
{
"queueEntryId": 12,
"jellyfinSourceId": "abc123",
"title": "Movie Title",
"mediaType": "movie",
"playback": {
"itemId": "abc123",
"sourceId": "abc123",
"title": "Movie Title",
"mediaType": "movie",
"streamUrl": "http://jellyfin:8096/Videos/abc123/stream?...",
"downloadUrl": "http://jellyfin:8096/Items/abc123/Download?...",
"durationSeconds": 5420,
"posterUrl": "https://...",
"year": "2024",
"tmdbId": "1234",
"imdbId": "tt1234567",
"episode": null
}
}
```
Notes:
- Movies resolve directly to their Jellyfin item IDs.
- Shows currently resolve to the first playable episode so the worker receives a real video target instead of a series ID.
- The worker should treat `playback.streamUrl` as the primary source and `downloadUrl` as a fallback.
### `POST /sessions/{sessionId}/control`
Supported actions:
- `pause`
- `resume`
- `seek`
- `skip`
- `stop`
### `GET /sessions/{sessionId}`
Worker status:
```json
{
"workerStatus": "connected",
"playbackState": "playing",
"currentTitle": "Movie Title",
"positionSeconds": 381,
"durationSeconds": 5420,
"lastError": null
}
```
## Milestones
### Milestone 1: Control Plane
- DB schema
- watch-party service layer
- dashboard/API endpoints
- queue and state transitions
### Milestone 2: Worker Stub
- second service in Docker Compose
- authenticated private API
- no real VC streaming yet
### Milestone 3: One-Item Playback
- connect to VC
- start one playable Jellyfin item from the control-plane playback contract
- stop/pause/resume
### Milestone 4: Real Watch Parties
- queue autoadvance
- seek/skip
- reconnect handling
- worker heartbeats
- dashboard controls
## Deployment Shape
Recommended services:
- `archive-status-bot`
- `orb-stream-worker`
The worker should not be publicly exposed. It should only be reachable on the internal Docker network.
## Known Risks
- Discord streaming requires a non-standard worker model and operational care.
- Some media will need transcoding.
- Playback control is substantially harder than catalog sync.
- Watch-party commands and Discord UX still need to be added after the worker exists.