# 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.