### Run owpctl setup with web UI
Source: https://mhbxyz.github.io/OpenWatchParty/operations/installation
Starts the local graphical assistant for guided installation. Uses the current user's configuration and Docker access. For system-wide installation, use 'sudo owpctl --scope system setup --web'.
```bash
owpctl setup --web
```
--------------------------------
### Quick Start: Clone and Run Development Environment
Source: https://mhbxyz.github.io/OpenWatchParty/development/setup
Clone the repository, configure git hooks with `just setup`, and start the development environment with `just up`. `just up` starts Jellyfin, builds and mounts the plugin, starts the Rust session server, auto-injects the client script, completes the Jellyfin setup wizard, provisions three local test users, and downloads two Blender open movies into a Jellyfin movie library.
```bash
# Clone the repository
git clone https://github.com/mhbxyz/OpenWatchParty.git
cd OpenWatchParty
# Configure git hooks (required once after clone)
just setup
# Start development environment
just up
```
--------------------------------
### Graphical Setup with owpctl setup --web
Source: https://mhbxyz.github.io/OpenWatchParty/operations/owpctl
Launches a web-based setup assistant that connects to an existing Jellyfin, previews operations, installs/upgrades the plugin, deploys the session-server image, configures authentication, and verifies health. Uses user scope and requires Docker access for that user. For system scope, use `sudo owpctl --scope system setup --web`. The assistant opens a one-time URL bound to 127.0.0.1 and stops after installation or 30 minutes.
```bash
owpctl setup --web
```
--------------------------------
### Start development environment with just up
Source: https://mhbxyz.github.io/OpenWatchParty/development/testing
Run this command to start the development environment before testing. It is the first step in the test environment setup.
```bash
just up
```
--------------------------------
### Headless Setup: Store Token, Review, Install
Source: https://mhbxyz.github.io/OpenWatchParty/operations/owpctl
Stores a temporary Jellyfin admin API token in a protected file, reviews the installation plan with --dry-run, then installs with --yes. The token file is removed after installation.
```bash
sudo install -m 0600 /dev/null /run/owp-jellyfin-token
sudo sh -c 'read -r token; printf %s "$token" > /run/owp-jellyfin-token'
sudo owpctl --scope system install --dry-run --api-token-file /run/owp-jellyfin-token
sudo owpctl --scope system install --yes --api-token-file /run/owp-jellyfin-token
sudo rm -f /run/owp-jellyfin-token
```
--------------------------------
### Set up development environment with just
Source: https://mhbxyz.github.io/OpenWatchParty/development/contributing
Sets up the development environment. The setup command configures git hooks and is required once; the up command starts the development environment.
```bash
just setup # Configure git hooks (required once)
just up
```
--------------------------------
### Set up development environment with just commands
Source: https://mhbxyz.github.io/OpenWatchParty
Sets up the OpenWatchParty development environment. Run 'just setup' once to configure git hooks, then use 'just up' to build and start Jellyfin and the session server. 'just dev' starts with log following, and 'just watch' enables auto-restart on file changes.
```bash
git clone https://github.com/mhbxyz/OpenWatchParty.git
cd OpenWatchParty
just setup # Configure git hooks (required once)
just up # Build and start Jellyfin + session server
just dev # Start with log following
just watch # Auto-restart on file changes
```
--------------------------------
### Headless Setup: Create Configuration
Source: https://mhbxyz.github.io/OpenWatchParty/operations/owpctl
Creates a non-interactive system-scope configuration for headless setup. Requires sudo and specifies the Jellyfin URL.
```bash
sudo owpctl --scope system setup \
--non-interactive \
--jellyfin-url https://jellyfin.example.com
```
--------------------------------
### Install mold linker
Source: https://mhbxyz.github.io/OpenWatchParty/development/setup
Install the mold linker for 5-10x faster linking. The project's .cargo/config.toml automatically uses mold when available.
```bash
# Arch Linux / Manjaro
sudo pacman -S mold
# Ubuntu/Debian
sudo apt install mold
# macOS (via Homebrew)
brew install mold
```
--------------------------------
### C# Project Version Configuration
Source: https://mhbxyz.github.io/OpenWatchParty/development/release
Example of the version element in a .csproj file.
```xml
0.2.0
```
--------------------------------
### Clone and start local demo with just
Source: https://mhbxyz.github.io/OpenWatchParty/product/first-watch-party
Commands to clone the OpenWatchParty repository, set up the environment, and start the services. Requires Docker, Docker Compose, just, Node.js 20+, Python 3, jq, curl, and unzip. Expected result: Jellyfin healthy at http://localhost:8096/web/, session server on port 3000, and Blender Open Movies library with Wing It! and Sprite Fright.
```bash
git clone https://github.com/mhbxyz/OpenWatchParty.git
cd OpenWatchParty
just setup
just up
just status
```
--------------------------------
### Environment File
Source: https://mhbxyz.github.io/OpenWatchParty/operations/deployment
Example .env file with JWT_SECRET placeholder and ALLOW_INSECURE_NO_AUTH setting.
```dotenv
# .env
# Paste the output of: openssl rand -base64 32
JWT_SECRET=
ALLOW_INSECURE_NO_AUTH=false
```
--------------------------------
### Restart Jellyfin after plugin installation
Source: https://mhbxyz.github.io/OpenWatchParty/operations/installation
Commands to restart Jellyfin after installing the plugin, for Docker and systemd.
```bash
# Docker
docker restart jellyfin
# Systemd
sudo systemctl restart jellyfin
```
--------------------------------
### CHANGELOG Format
Source: https://mhbxyz.github.io/OpenWatchParty/development/release
Example of the changelog format used for releases.
```markdown
# Changelog
## [0.2.0] - 2024-01-15
### Added
- New feature description
### Changed
- Change description
### Fixed
- Bug fix description
### Security
- Security fix description
## [0.1.0] - 2024-01-01
Initial release.
```
--------------------------------
### External Application API Usage (curl)
Source: https://mhbxyz.github.io/OpenWatchParty/technical/api
Example shell commands to authenticate with Jellyfin and then get an OpenWatchParty token. The first command authenticates and extracts the access token using jq, the second uses that token to request the OpenWatchParty token.
```bash
# Authenticate with Jellyfin first
TOKEN=$(curl -s -X POST "http://localhost:8096/Users/AuthenticateByName" \
-H "X-Emby-Authorization: MediaBrowser Client=\"App\", Device=\"CLI\", DeviceId=\"abc\", Version=\"1.0\"" \
-H "Content-Type: application/json" \
-d '{"Username":"admin","Pw":"password"}' | jq -r '.AccessToken')
# Get OpenWatchParty token
curl -H "Authorization: MediaBrowser Token=\"$TOKEN\"" \
"http://localhost:8096/OpenWatchParty/Token"
```
--------------------------------
### Docker Compose Production Setup
Source: https://mhbxyz.github.io/OpenWatchParty/operations/deployment
Complete docker-compose.prod.yml file defining services for Jellyfin, session-server, and Caddy with networking and volumes.
```yaml
# docker-compose.prod.yml
version: '3.8'
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
volumes:
- ./config:/config
- ./cache:/cache
- /path/to/media:/media:ro
- ./plugins/OpenWatchParty:/config/plugins/OpenWatchParty:ro
environment:
- JELLYFIN_PublishedServerUrl=https://jellyfin.example.com
restart: unless-stopped
networks:
- internal
session-server:
image: owp-session-server
container_name: owp-session
environment:
- ALLOWED_ORIGINS=https://jellyfin.example.com
- JWT_SECRET=${JWT_SECRET}
- ALLOW_INSECURE_NO_AUTH=false
- LOG_LEVEL=warn
restart: unless-stopped
networks:
- internal
caddy:
image: caddy:2-alpine
container_name: caddy
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
restart: unless-stopped
networks:
- internal
networks:
internal:
volumes:
caddy_data:
caddy_config:
```
--------------------------------
### client_hello message JSON example
Source: https://mhbxyz.github.io/OpenWatchParty/technical/protocol
Sent immediately after WebSocket connection. Contains client ID and timestamps.
```json
{
"type": "client_hello",
"client": "uuid-client-id",
"payload": {
"client_id": "uuid-client-id"
},
"ts": 1678900000000,
"server_ts": 1678900000000
}
```
--------------------------------
### Commit message examples
Source: https://mhbxyz.github.io/OpenWatchParty/development/contributing
Examples of valid commit messages using the required format. Each message starts with a type (feat, fix, docs) followed by a brief description.
```text
feat: add participant count to room list
fix: prevent feedback loop in HLS buffering
docs: update installation instructions
```
--------------------------------
### GET /OpenWatchParty/Token response (insecure mode)
Source: https://mhbxyz.github.io/OpenWatchParty/technical/api
Example JSON response when insecure development mode is explicitly enabled. The token is null and auth_enabled is false, with insecure_mode set to true.
```json
{
"token": null,
"auth_enabled": false,
"insecure_mode": true,
"user_id": "abc123",
"user_name": "John",
"session_server_url": "wss://jellyfin.example.com/ws",
"hide_native_syncplay_button": false,
"show_sync_nudge_button": false,
"invite_ttl_seconds": 3600,
"protocol_version": 1
}
```
--------------------------------
### GET /OpenWatchParty/Token response (authenticated)
Source: https://mhbxyz.github.io/OpenWatchParty/technical/api
Example JSON response from the Token endpoint when authentication is successful. Contains the JWT token, authentication flags, session server URL, and configuration settings.
```json
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"auth_enabled": true,
"expires_in": 3600,
"user_id": "abc123",
"user_name": "John",
"session_server_url": "wss://jellyfin.example.com/ws",
"hide_native_syncplay_button": false,
"show_sync_nudge_button": false,
"invite_ttl_seconds": 3600,
"protocol_version": 1
}
```
--------------------------------
### Build All Components
Source: https://mhbxyz.github.io/OpenWatchParty/development/release
Command to build all components using just.
```bash
just build
```
--------------------------------
### JavaScript API Usage
Source: https://mhbxyz.github.io/OpenWatchParty/technical/api
Example JavaScript functions to get a token and load the client script. The getToken function fetches '/OpenWatchParty/Token' with credentials included to pass Jellyfin auth cookies, and throws an error if the request fails.
```javascript
// Get token
async function getToken() {
const response = await fetch('/OpenWatchParty/Token', {
credentials: 'include' // Include Jellyfin auth cookies
});
if (!response.ok) {
throw new Error(`Token request failed: ${response.status}`);
}
return await response.json();
}
// Load client script
function loadClientScript() {
const script = document.createElement('script');
script.src = '/OpenWatchParty/ClientScript';
document.head.appendChild(script);
}
```
--------------------------------
### Initialization flow for SyncPlay
Source: https://mhbxyz.github.io/OpenWatchParty/technical/jellyfin-syncplay-reference
Steps to enable SyncPlay, force time sync update, wait for server update event, set ready flag, and process queued commands.
```text
1. SyncPlay enabled
2. timeSyncCore.forceUpdate()
3. Wait for 'time-sync-server-update' event
4. syncPlayReady = true
5. Process queued commands
```
--------------------------------
### Build and run session server from source with Docker
Source: https://mhbxyz.github.io/OpenWatchParty/operations/installation
Commands to build the session server Docker image from source and run it. Requires Docker and the source repository.
```bash
# Build the image
docker build --build-arg BUILD_MODE=release \
-f infra/docker/server.Dockerfile \
-t owp-session-server ./src/server
# Run the container
JWT_SECRET="$(openssl rand -base64 32)"
printf 'Configure this same JWT secret in the OpenWatchParty plugin: %s\n' "$JWT_SECRET"
docker run -d \
--name owp-session \
-p 3000:3000 \
-e ALLOWED_ORIGINS="http://localhost:8096" \
-e JWT_SECRET="$JWT_SECRET" \
owp-session-server
```
--------------------------------
### Build Individually
Source: https://mhbxyz.github.io/OpenWatchParty/development/release
Commands to build the Rust session server and the C# plugin individually.
```bash
# Rust session server
cd src/server
cargo build --release
# C# plugin
cd src/plugins/jellyfin/OpenWatchParty
dotnet build -c Release
```
--------------------------------
### Minimal Setup (Development) - docker-compose.yml
Source: https://mhbxyz.github.io/OpenWatchParty/operations/configuration
Development configuration for the session server. Enables insecure unauthenticated access via ALLOW_INSECURE_NO_AUTH=true. Plugin settings: JWT Secret empty, Allow insecure unauthenticated development enabled, Session Server URL empty, Trust automatic same-host port 3000 session server enabled. Existing installations with an empty plugin secret are blocked after upgrading; configure the same secret or explicitly opt into insecure mode for local development only.
```yaml
# docker-compose.yml
services:
session-server:
image: owp-session-server
ports:
- "3000:3000"
environment:
- ALLOW_INSECURE_NO_AUTH=true
```
--------------------------------
### Plugin.cs - Jellyfin Plugin Entry Point
Source: https://mhbxyz.github.io/OpenWatchParty/technical/plugin
Plugin entry point implementing BasePlugin and IHasWebPages. Sets the singleton Instance, logs an error if JwtSecret is not configured, and exposes the admin config page as an embedded resource.
```csharp
public class Plugin : BasePlugin, IHasWebPages
{
// Singleton instance - standard Jellyfin plugin pattern
public static Plugin? Instance { get; private set; }
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILogger logger)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
// Log JWT configuration status
if (string.IsNullOrWhiteSpace(Configuration.JwtSecret))
{
_logger.LogError("[OpenWatchParty] JwtSecret not configured. Token issuance blocked.");
}
}
public override string Name => "OpenWatchParty";
public override Guid Id => new("0f2fd0fd-09ff-4f49-9f1c-4a8f421a4b7d");
public IEnumerable GetPages()
{
return new[]
{
new PluginPageInfo
{
Name = "OpenWatchParty",
EmbeddedResourcePath = GetType().Namespace + ".Web.configPage.html"
}
};
}
}
```
--------------------------------
### Backup before upgrade
Source: https://mhbxyz.github.io/OpenWatchParty/operations/deployment
Stops all services and creates a tar.gz archive of the config directory. This is the first step in the upgrade procedure to ensure a rollback point.
```bash
docker compose stop
tar -czf backup-before-upgrade.tar.gz ./config
```
--------------------------------
### Docker Compose example for session server
Source: https://mhbxyz.github.io/OpenWatchParty/operations/configuration
Example docker-compose service definition for the session server, setting environment variables like ALLOWED_ORIGINS, JWT_SECRET, ALLOW_INSECURE_NO_AUTH, and LOG_LEVEL.
```yaml
services:
session-server:
image: owp-session-server
ports:
- "3000:3000"
environment:
- ALLOWED_ORIGINS=https://jellyfin.example.com
- JWT_SECRET=${JWT_SECRET}
- ALLOW_INSECURE_NO_AUTH=false
- LOG_LEVEL=info
restart: unless-stopped
```
--------------------------------
### GET plugin configuration with curl
Source: https://mhbxyz.github.io/OpenWatchParty/technical/api
Retrieves the current plugin configuration using the Jellyfin API endpoint GET /System/Configuration/Plugin/{pluginId}. Requires an admin token in the Authorization header.
```bash
curl -H "Authorization: MediaBrowser Token=\"$TOKEN\"" \
"http://localhost:8096/System/Configuration/Plugin/0f2fd0fd-09ff-4f49-9f1c-4a8f421a4b7d"
```
--------------------------------
### Run CI checks locally with just commands
Source: https://mhbxyz.github.io/OpenWatchParty/development/contributing
Commands to format, lint, test, and build all components using just.
```bash
just fmt # Format Rust code
just lint # Run linters
just test # Run tests
just build # Build all components
```
--------------------------------
### Rate formula and examples
Source: https://mhbxyz.github.io/OpenWatchParty/technical/sync
Shows the progressive sqrt rate formula: rate = 1 + sign(drift) * sqrt(|drift|) * DRIFT_GAIN (0.15). Includes examples for positive and negative drift, with clamping to 0.90x-1.15x.
```text
rate = 1 + sign(drift) * sqrt(|drift|) * DRIFT_GAIN
= 1 + sign(drift) * sqrt(|drift|) * 0.15
Examples:
- drift = +0.25s → rate = 1 + sqrt(0.25) * 0.15 = 1.075x
- drift = +1.0s → rate = 1 + sqrt(1.0) * 0.15 = 1.15x
- drift = +1.9s → rate = 1 + sqrt(1.9) * 0.15 = 1.21x (capped to 1.15x)
- drift = -0.1s → rate = 1 - sqrt(0.1) * 0.15 = 0.95x
- drift = -0.5s → rate = 1 - sqrt(0.5) * 0.15 = 0.89x (clamped to 0.90x)
```
--------------------------------
### Download owpctl for Linux x86_64
Source: https://mhbxyz.github.io/OpenWatchParty/operations/owpctl
Downloads the owpctl binary for Linux x86_64, verifies its SHA256 checksum, makes it executable, and installs it to /usr/local/bin. On macOS, verify with `shasum -a 256 -c` instead.
```bash
# Linux x86_64 example
curl -fLO https://github.com/mhbxyz/OpenWatchParty/releases/latest/download/owpctl-linux-x86_64
curl -fLO https://github.com/mhbxyz/OpenWatchParty/releases/latest/download/owpctl-linux-x86_64.sha256
sha256sum -c owpctl-linux-x86_64.sha256
chmod +x owpctl-linux-x86_64
sudo install -m 0755 owpctl-linux-x86_64 /usr/local/bin/owpctl
```
--------------------------------
### Multi-Instance Setup - docker-compose.yml
Source: https://mhbxyz.github.io/OpenWatchParty/operations/configuration
Configuration for high availability or multiple Jellyfin instances. Sets allowed origins for multiple domains and uses deploy replicas. Note: The session server is stateful (in-memory rooms), so only one instance should run. For scaling, consider adding persistence (planned feature).
```yaml
services:
session-server:
image: owp-session-server
deploy:
replicas: 1 # Single instance (stateful)
environment:
- ALLOWED_ORIGINS=https://jellyfin1.example.com,https://jellyfin2.example.com
```
--------------------------------
### Log Format Example
Source: https://mhbxyz.github.io/OpenWatchParty/operations/monitoring
Shows the log format with timestamp, level, module, and key=value fields like client_id and room_id. Values from clients or errors are quoted and escaped.
```text
[2026-10-06T15:46:15Z INFO session_server::ws::connection] Client connected client_id=4f2a... auth_required=true
[2026-10-06T15:46:15Z INFO session_server::ws::handlers::auth] Client authenticated client_id=4f2a... user="alice"
[2026-10-06T15:47:02Z INFO session_server::ws::handlers::create] Creating room room_id=9c1e... client_id=4f2a... name="alice's room"
```
--------------------------------
### Cargo.toml Version Configuration
Source: https://mhbxyz.github.io/OpenWatchParty/development/release
Example of the version field in the Rust Cargo.toml file.
```toml
[package]
version = "0.2.0"
```
--------------------------------
### participants_update message JSON example
Source: https://mhbxyz.github.io/OpenWatchParty/technical/protocol
Participant count update. Contains the new participant_count.
```json
{
"type": "participants_update",
"room": "uuid-room-id",
"payload": {
"participant_count": 4
},
"ts": 1678900000000,
"server_ts": 1678900000000
}
```
--------------------------------
### Rust test module for all_ready function
Source: https://mhbxyz.github.io/OpenWatchParty/development/testing
Example unit tests for the all_ready function, covering an empty room and a partially ready room. Uses a Room struct with clients and ready_clients fields.
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_all_ready_empty_room() {
let room = Room {
clients: vec![],
ready_clients: HashSet::new(),
..Default::default()
};
assert!(all_ready(&room));
}
#[test]
fn test_all_ready_partial() {
let room = Room {
clients: vec!["a".into(), "b".into()],
ready_clients: HashSet::from(["a".into()]),
..Default::default()
};
assert!(!all_ready(&room));
}
}
```
--------------------------------
### CORS configuration examples for ALLOWED_ORIGINS
Source: https://mhbxyz.github.io/OpenWatchParty/operations/configuration
Shows how to set ALLOWED_ORIGINS for single origin, multiple origins, and development wildcard. Using '*' logs a security warning and is not recommended for production.
```bash
# Single origin
ALLOWED_ORIGINS=https://jellyfin.example.com
# Multiple origins
ALLOWED_ORIGINS=https://jellyfin.example.com,http://localhost:8096
# Development (not for production!)
ALLOWED_ORIGINS=*
```
--------------------------------
### error message JSON example
Source: https://mhbxyz.github.io/OpenWatchParty/technical/protocol
Error response with stable machine-readable code and human-readable message.
```json
{
"type": "error",
"payload": {
"code": "ROOM_NOT_FOUND",
"message": "Error description"
},
"ts": 1678900000000,
"server_ts": 1678900000000
}
```
--------------------------------
### Restart services after upgrade
Source: https://mhbxyz.github.io/OpenWatchParty/operations/deployment
Starts the services in detached mode after the upgrade. This is the fourth step in the upgrade procedure.
```bash
docker compose up -d
```
--------------------------------
### Architecture Overview Diagram
Source: https://mhbxyz.github.io/OpenWatchParty/operations/deployment
ASCII diagram showing the production architecture: Internet -> Reverse Proxy -> Jellyfin and Session Server.
```text
Internet
│
▼
┌─────────────────────────────────────┐
│ Reverse Proxy │
│ (nginx/Caddy/Traefik) │
│ SSL termination, routing │
└───────────┬─────────────┬───────────┘
│ │
┌───────▼───────┐ ┌───▼───────────┐
│ Jellyfin │ │ Session Server│
│ :8096 │ │ :3000 │
└───────────────┘ └───────────────┘
```
--------------------------------
### chat_message message JSON example
Source: https://mhbxyz.github.io/OpenWatchParty/technical/protocol
Chat message broadcast from server. Payload contains username and text.
```json
{
"type": "chat_message",
"room": "uuid-room-id",
"client": "uuid-sender-id",
"payload": {
"username": "Alice",
"text": "Hello everyone!"
},
"ts": 1678900000000,
"server_ts": 1678900000050
}
```
--------------------------------
### client_left message JSON example
Source: https://mhbxyz.github.io/OpenWatchParty/technical/protocol
Sent when a participant leaves the room. Payload includes updated participant_count.
```json
{
"type": "client_left",
"room": "uuid-room-id",
"client": "uuid-left-client-id",
"payload": {
"participant_count": 2
},
"ts": 1678900000000,
"server_ts": 1678900000000
}
```