TheOrb/WATCHPARTY_IMPLEMENTATION_PLAN.md

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

  • 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:

{
  "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.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:

{
  "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.