4.9 KiB
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
idguild_idvoice_channel_idtext_channel_idowner_user_idtitlestatus(draft,queued,connecting,playing,paused,stopped,error)worker_session_idcurrent_queue_entry_idcreated_atupdated_at
watch_party_queue
idsession_idpositionmedia_typejellyfin_source_idtitleyearruntimesummaryposter_urlstatus(queued,playing,played,skipped,failed)created_at
watch_party_events
idsession_idevent_typepayload_jsoncreated_at
watch_party_worker_state
session_idworker_statusplayback_statecurrent_titleposition_secondsduration_secondslast_errorupdated_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:
{
"sessionId": "local-session-id",
"guildId": "123",
"voiceChannelId": "456",
"textChannelId": "789"
}
POST /sessions/{sessionId}/play
Start one Jellyfin item.
Request:
{
"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.streamUrlas the primary source anddownloadUrlas a fallback.
POST /sessions/{sessionId}/control
Supported actions:
pauseresumeseekskipstop
GET /sessions/{sessionId}
Worker status:
{
"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-botorb-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.