240 lines
4.9 KiB
Markdown
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.
|