### 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 } ```