diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..b3d6fb60 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +/target +/target-patched +/libtorrent-downloads +/rqbit-downloads +/vcpkg +/vcpkg_installed +err.txt +/System.Collections.Hashtable.Root/ +.git +docs +scripts/tests +*.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..17fcb5ef --- /dev/null +++ b/Dockerfile @@ -0,0 +1,78 @@ +# syntax=docker/dockerfile:1 +# +# Multi-stage build for stream-server with the libtorrent backend. +# Mirrors the "check"/"build-linux" jobs in .github/workflows/release.yml: +# same base OS family (Debian/Ubuntu), same libtorrent version, same +# cargo feature flags, so the image matches what CI already validates. + +######################################## +# Stage 1: cargo-chef base +######################################## +FROM rust:1-bookworm AS chef +RUN cargo install cargo-chef --locked +WORKDIR /app + +######################################## +# Stage 2: dependency recipe (for Docker layer caching) +######################################## +FROM chef AS planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +######################################## +# Stage 3: builder +######################################## +FROM chef AS builder + +ENV DEBIAN_FRONTEND=noninteractive \ + LIBTORRENT_STATIC=1 \ + PKG_CONFIG_PATH=/usr/local/lib/pkgconfig + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake curl pkg-config \ + libssl-dev libboost-dev libclang-dev \ + libgtk-3-dev libayatana-appindicator3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Build libtorrent-rasterbar 2.1.1 from source, statically, the same way CI does. +COPY .github/scripts/install-libtorrent.sh .github/scripts/install-libtorrent.sh +RUN bash .github/scripts/install-libtorrent.sh + +# Cache dependency compilation separately from application code. +COPY --from=planner /app/recipe.json recipe.json +RUN cargo chef cook --release --recipe-path recipe.json --features libtorrent --no-default-features + +COPY . . +RUN cargo build --release --features libtorrent --no-default-features + +######################################## +# Stage 4: runtime +######################################## +FROM debian:bookworm-slim AS runtime + +ENV DEBIAN_FRONTEND=noninteractive \ + RUST_LOG=info + +# server links against the desktop-integration stack (tray icon / settings GUI) +# on all cfg(unix/windows/macos) targets, so the runtime image needs the +# shared libraries those bring in even though it starts headless. Installing +# the -dev metapackages (rather than hand-picking .so names) guarantees every +# transitive runtime library apt would install for the equivalent build +# dependency is present too, avoiding "error while loading shared libraries" +# at startup. +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates ffmpeg \ + libssl-dev libboost-dev libgtk-3-dev libayatana-appindicator3-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/target/release/server /usr/local/bin/stream-server + +# HTTP API / streaming, HTTPS, and the default BitTorrent listen range +# (btListenInterfaces, configurable via /settings). +EXPOSE 11470/tcp 12470/tcp 42000-42010/tcp 42000-42010/udp + +VOLUME ["/root/.config/stremio-server", "/root/.cache/stremio-server"] + +# --no-tray forces the headless code path (no tray/GUI init), since the +# release binary otherwise defaults to tray mode, which requires a display. +ENTRYPOINT ["/usr/local/bin/stream-server", "--no-tray"] diff --git a/README.md b/README.md index c1d26bac..c0fd12f1 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,133 @@ The server starts on `http://localhost:11470` by default (compatible with standa --- +## 🐳 Docker + +A multi-stage `Dockerfile` builds the `libtorrent` backend the same way CI does (libtorrent 2.1.1 from source, `--features libtorrent --no-default-features`): + +```bash +docker build -t stream-server:libtorrent . + +docker run -d --name stream-server \ + -p 11470:11470 \ + -p 42000-42010:42000-42010/tcp \ + -p 42000-42010:42000-42010/udp \ + -v stream-server-config:/root/.config/stremio-server \ + -v stream-server-cache:/root/.cache/stremio-server \ + stream-server:libtorrent +``` + +- The container starts headless automatically (equivalent to `stream-server --no-tray`) — no display server required. +- Mount `/root/.config/stremio-server` (settings, HTTPS certs, logs) and `/root/.cache/stremio-server` (downloads/cache) as volumes so they survive container recreation. `HOME`/`XDG_CONFIG_HOME`/`XDG_CACHE_HOME`/`XDG_DATA_HOME` are also honored if you'd rather point everything at one mounted directory. +- BitTorrent peer traffic defaults to ports `42000-42010` (TCP+UDP, `btListenInterfaces` in `/settings`). Publish that range (or run with `--network host`) for good peer connectivity. +- `RUST_LOG` is set to `info` in the image; override with `-e RUST_LOG=debug` for more verbose logs. +- Pass `--tui` as an extra image argument (with `docker run -it`, or `-d -it` to attach later) for the built-in terminal stats dashboard — see [Accessing & Configuring the UI](#-accessing--configuring-the-ui). + +### `--network host` + +The default ports (`11470`/`12470`) or BitTorrent range (`42000-42010`) may already be taken by something else on the host (another stream-server instance, Stremio's own `server.js`, etc.) — this is common if you're running this alongside other services. Since `--network host` means Docker can no longer remap ports for you, the ports are overridable via environment variables: + +| Variable | Default | Purpose | +| --- | --- | --- | +| `STREAM_SERVER_HTTP_PORT` | `11470` | HTTP API/streaming port | +| `STREAM_SERVER_HTTPS_PORT` | `12470` | HTTPS port (only serves if certs exist in the config dir) | +| `STREAM_SERVER_PUBLIC_BASE_URL` | `http://127.0.0.1:` | Base URL the server reports about itself, e.g. in `/settings` | + +```bash +docker run -d -it --name stream-server \ + --network host \ + --restart unless-stopped \ + -v stream-server-data:/data \ + -e HOME=/data -e XDG_CONFIG_HOME=/data/config -e XDG_CACHE_HOME=/data/cache -e XDG_DATA_HOME=/data/share \ + -e STREAM_SERVER_HTTP_PORT=11472 -e STREAM_SERVER_HTTPS_PORT=12472 \ + -e STREAM_SERVER_PUBLIC_BASE_URL=http://192.168.1.5:11472 \ + --device /dev/dri:/dev/dri \ + stream-server:libtorrent --tui +``` + +`--device /dev/dri:/dev/dri` passes through the host GPU for hardware-accelerated FFmpeg transcoding, if available. `-it` (kept even when running detached with `-d`) allocates a pty so `docker attach` can show the `--tui` dashboard later without disrupting the running server (see below). + +This exact configuration has been built and deployed end-to-end (image build, container run, `/heartbeat`, `/settings` tuning, and TUI attach all verified) on an x86_64 Fedora/Docker host running alongside other, unrelated streaming services. Add `STREAM_SERVER_BT_*` env vars from [Tuning for playback speed](#tuning-for-playback-speed) here too if you want them baked into the deployment — note that they only take effect on a *fresh* volume/config dir; `stream-server-data` above is reused on purpose to carry over an existing deployment's cache and settings, so its already-persisted `settings.json` takes precedence regardless of env vars. + +## 🖥️ Accessing & Configuring the UI + +Stream Server has **no bundled web UI** — it's a drop-in backend that a Stremio client (web, desktop, or mobile app) talks to, the same way it talks to Stremio's official `server.js`. To use it: + +1. Open the [Stremio web app](https://web.stremio.com) or a desktop/mobile Stremio client. +2. Go to **Settings → Streaming Server**. +3. Set **Server URL** to where this container/binary is reachable, e.g. `http://192.168.1.5:11470/` (swap in your host's IP/port). +4. Stremio should report the server as connected; playing a torrent stream will now route through this server instead of Stremio's built-in one. + +It does have a **built-in terminal (TUI) stats dashboard** — a live view of active engines/torrents and files, built with `ratatui`. Start the binary/container with `--tui` (alongside `--no-tray` for headless/Docker use) to enable it: + +```bash +# Native +stream-server --no-tray --tui + +# Docker: run with -it (even when backgrounded with -d) so a pty exists to attach to +docker run -d -it --name stream-server ... stream-server:libtorrent --tui +docker attach stream-server # view the live dashboard +# detach without stopping the container: Ctrl+P, Ctrl+Q +``` + +The TUI needs a real terminal (it's a no-op, non-fatal, if none is attached), so it doesn't fit a plain `docker run -d` without `-it`. + +For remote/scripted access instead, use the JSON endpoints directly: + +```bash +curl http://192.168.1.5:11470/heartbeat # health check +curl http://192.168.1.5:11470/stats.json # server + torrent stats +curl http://192.168.1.5:11470/settings # current settings, including BitTorrent options +``` + +If desktop-app config is what you want instead, `settings-gui` (built alongside `server` on Windows/macOS/Linux desktop builds) provides a native settings window — not applicable to the headless Docker image above. + +## ⚙️ Configuring libtorrent + +BitTorrent/libtorrent behavior is configured at runtime via the same `/settings` API used above, and persisted to `settings.json` in the config directory (`~/.config/stremio-server/settings.json` on Linux, or wherever your config volume/`XDG_CONFIG_HOME` points in Docker) — no rebuild needed. Full option reference: [docs/bittorrent-settings.md](docs/bittorrent-settings.md). + +```bash +# View current settings +curl http://192.168.1.5:11470/settings + +# Update just the keys you want to change +curl -X POST http://192.168.1.5:11470/settings \ + -H "Content-Type: application/json" \ + -d '{"btEnableDht": false, "btEncryptionMode": "require"}' +``` + +Key options: `btEnableDht`, `btEnablePex`, `btEnableLsd` (peer discovery), `btEncryptionMode` (`allow`/`require`/`disable`), `btAnonymousMode`, `btAllowMultipleConnectionsPerIp`, `btValidateHttpsTrackers`, `btSsrfMitigation`. + +### Tuning for playback speed + +Every field in `/settings` can also be set as a `STREAM_SERVER_*` environment variable (e.g. `btMaxConnections` → `STREAM_SERVER_BT_MAX_CONNECTIONS`, camelCase → SCREAMING_SNAKE_CASE), which is read once when the server constructs its **default** settings on first startup. This only shapes the *initial* config for a fresh deployment (no `settings.json` yet in the config dir) — once `settings.json` exists, it's the source of truth and `/settings` (below) is how you change things from then on. + +The shipped defaults are already tuned for a single-stream home connection in the ~100Mbps class: + +| Env var | Default | Why | +| --- | --- | --- | +| `STREAM_SERVER_BT_MAX_CONNECTIONS` | `800` | Per-torrent connections are separately clamped to 200 once the global max reaches 800 (`TorrentSpeedProfile::effective_connection_limits`), so raising this further doesn't speed up a single stream — it only helps if you're running several torrents at once. | +| `STREAM_SERVER_BT_MIN_PEERS_FOR_STABLE` | `3` (was `5`) | The engine reports "stable" (and playback can start) with fewer peers connected — a little less early-playback smoothness for a faster start. | +| `STREAM_SERVER_BT_ALLOW_MULTIPLE_CONNECTIONS_PER_IP` | `true` (was `false`) | More usable connections when many peers sit behind the same IP (CGNAT, seedboxes). | +| `STREAM_SERVER_BT_ENCRYPTION_MODE` | `allow` | Accepts both encrypted and plaintext peers, maximizing the usable peer pool. Only use `require` if your ISP throttles unencrypted BitTorrent traffic. | +| `STREAM_SERVER_BT_DOWNLOAD_SPEED_HARD_LIMIT` | `11796480` (~11.25MB/s, bytes/sec) | ~90% of a 100Mbps line. This is the one speed-limit field actually wired to libtorrent (`download_rate_limit`); leaving headroom below your real line rate avoids bufferbloat, which costs more effective throughput — and playback smoothness — than it gains. **Adjust this for your own link speed**: roughly `(Mbps × 1_000_000 / 8) × 0.9`. Set to `0` for unlimited. | +| `STREAM_SERVER_BT_DOWNLOAD_SPEED_SOFT_LIMIT` | `0` | Present in the API for parity, but not currently wired to anything in the libtorrent backend — changing it has no effect yet. | +| `STREAM_SERVER_SEEDING_ENABLED` | `true` | Keeps seeding after download completes, which improves download speed too via tit-for-tat reciprocity with peers. | + +All other `ServerSettings` fields (`btEnableDht`/`Pex`/`Lsd`, `btAnonymousMode`, `btListenInterfaces`, `btOutgoing*`, the `btProxy*` group, `btValidateHttpsTrackers`, `btSsrfMitigation`, `cacheSize`, `proxyStreamsEnabled`) follow the same `STREAM_SERVER_` pattern — see `server/src/routes/system.rs`'s `Default for ServerSettings` for the exact list. Don't set `btSsrfMitigation`/`btValidateHttpsTrackers` to `false` chasing speed — they don't meaningfully affect throughput, only safety. + +Equivalent one-off tuning via the running API instead of env vars (useful for a change you don't want to bake into the deployment): + +```bash +curl -X POST http://192.168.1.5:11470/settings \ + -H "Content-Type: application/json" \ + -d '{"btMaxConnections": 800, "btDownloadSpeedHardLimit": 11796480}' +``` + +Beyond `/settings`, run with `--network host` and publish/forward the `btListenInterfaces` port range — libtorrent's UPnP/NAT-PMP (always on) can then successfully port-map with your router for real incoming connections, which matters more for peer availability than any single setting above. + +--- + ## 🔧 Build Instructions
diff --git a/server/src/lib.rs b/server/src/lib.rs index 69db2f91..d8488d48 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -112,10 +112,26 @@ impl ServerConfig { } pub fn binary_default() -> Self { + // Overridable so multiple instances (or instances sharing a host with + // other services on the default ports) can run with --network host, + // where Docker can no longer remap ports for us. + let http_port = std::env::var("STREAM_SERVER_HTTP_PORT") + .ok() + .and_then(|v| v.parse::().ok()) + .unwrap_or(DEFAULT_HTTP_PORT); + let https_port = std::env::var("STREAM_SERVER_HTTPS_PORT") + .ok() + .and_then(|v| v.parse::().ok()) + .unwrap_or(DEFAULT_HTTPS_PORT); + let public_base_url = std::env::var("STREAM_SERVER_PUBLIC_BASE_URL") + .ok() + .filter(|v| !v.is_empty()) + .unwrap_or_else(|| format!("http://127.0.0.1:{http_port}")); + Self { - http_addr: SocketAddr::from(([0, 0, 0, 0], DEFAULT_HTTP_PORT)), - https_addr: Some(SocketAddr::from(([0, 0, 0, 0], DEFAULT_HTTPS_PORT))), - public_base_url: Some(format!("http://127.0.0.1:{DEFAULT_HTTP_PORT}")), + http_addr: SocketAddr::from(([0, 0, 0, 0], http_port)), + https_addr: Some(SocketAddr::from(([0, 0, 0, 0], https_port))), + public_base_url: Some(public_base_url), config_dir: None, cache_dir: None, use_tui: false, diff --git a/server/src/routes/system.rs b/server/src/routes/system.rs index 4cea28ae..543c8aee 100644 --- a/server/src/routes/system.rs +++ b/server/src/routes/system.rs @@ -366,6 +366,46 @@ fn update_string_setting( } } +/// Reads `key` from the environment; falls back to `default` if unset or unparseable. +fn env_bool(key: &str, default: bool) -> bool { + match std::env::var(key) { + Ok(v) => match v.trim().to_ascii_lowercase().as_str() { + "1" | "true" | "yes" | "on" => true, + "0" | "false" | "no" | "off" => false, + _ => default, + }, + Err(_) => default, + } +} + +fn env_parsed(key: &str, default: T) -> T { + std::env::var(key) + .ok() + .and_then(|v| v.trim().parse().ok()) + .unwrap_or(default) +} + +fn env_string(key: &str, default: String) -> String { + std::env::var(key) + .ok() + .filter(|v| !v.is_empty()) + .unwrap_or(default) +} + +fn env_encryption_mode(key: &str, default: TorrentEncryptionMode) -> TorrentEncryptionMode { + std::env::var(key) + .ok() + .and_then(|v| parse_torrent_encryption_mode(&Value::String(v))) + .unwrap_or(default) +} + +fn env_proxy_type(key: &str, default: TorrentProxyType) -> TorrentProxyType { + std::env::var(key) + .ok() + .and_then(|v| parse_torrent_proxy_type(&Value::String(v))) + .unwrap_or(default) +} + impl Default for ServerSettings { fn default() -> Self { let cache_root = std::env::var("STREMIO_CACHE_ROOT") @@ -377,41 +417,95 @@ impl Default for ServerSettings { .to_string() }); + // Every field below is overridable via a STREAM_SERVER_* env var, read + // once here at first startup (before any settings.json exists). Once + // settings.json exists, it takes over and the /settings API is the + // way to change things -- these env vars only shape the *initial* + // defaults for a fresh deployment. Baseline values below are tuned + // for a single-stream home connection in the ~100Mbps class: + // bt_max_connections=800 already saturates the per-torrent cap + // (see enginefs::backend::TorrentSpeedProfile::effective_connection_limits, + // which clamps per-torrent connections to 200 once the global max + // reaches 800), so raising it further only helps if you're running + // several torrents at once -- it doesn't speed up a single stream. + // bt_download_speed_hard_limit defaults to ~90% of a 100Mbps line + // (11.25 MB/s) to leave headroom against bufferbloat/self-induced + // congestion, which otherwise costs more effective throughput (and + // playback smoothness) than it gains; set it to 0 to go unlimited. Self { app_path: std::env::current_exe() .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|_| "/usr/bin/stremio-server".to_string()), server_version: env!("CARGO_PKG_VERSION").to_string(), cache_root, - cache_size: 10.0 * 1024.0 * 1024.0 * 1024.0, // 10GB - proxy_streams_enabled: false, - bt_max_connections: enginefs::backend::DEFAULT_BT_MAX_CONNECTIONS, - bt_handshake_timeout: 20000, - bt_request_timeout: 10000, - bt_download_speed_soft_limit: 0.0, - bt_download_speed_hard_limit: 0.0, - bt_min_peers_for_stable: 5, - bt_enable_dht: default_bt_enable_dht(), - bt_enable_pex: default_bt_enable_pex(), - bt_enable_lsd: default_bt_enable_lsd(), - bt_encryption_mode: TorrentEncryptionMode::default(), - bt_anonymous_mode: default_bt_anonymous_mode(), - bt_allow_multiple_connections_per_ip: default_bt_allow_multiple_connections_per_ip(), - bt_listen_interfaces: default_bt_listen_interfaces(), - bt_outgoing_interfaces: String::new(), - bt_outgoing_port: 0, - bt_num_outgoing_ports: 0, - bt_proxy_type: TorrentProxyType::default(), - bt_proxy_host: String::new(), - bt_proxy_port: 0, - bt_proxy_username: String::new(), - bt_proxy_password: String::new(), - bt_proxy_hostnames: default_bt_proxy_hostnames(), - bt_proxy_peer_connections: default_bt_proxy_peer_connections(), - bt_proxy_tracker_connections: default_bt_proxy_tracker_connections(), - bt_proxy_send_host_in_connect: default_bt_proxy_send_host_in_connect(), - bt_validate_https_trackers: default_bt_validate_https_trackers(), - bt_ssrf_mitigation: default_bt_ssrf_mitigation(), + cache_size: env_parsed("STREAM_SERVER_CACHE_SIZE_BYTES", 10.0 * 1024.0 * 1024.0 * 1024.0), // 10GB + proxy_streams_enabled: env_bool("STREAM_SERVER_PROXY_STREAMS_ENABLED", false), + bt_max_connections: env_parsed( + "STREAM_SERVER_BT_MAX_CONNECTIONS", + enginefs::backend::DEFAULT_BT_MAX_CONNECTIONS, + ), + bt_handshake_timeout: env_parsed("STREAM_SERVER_BT_HANDSHAKE_TIMEOUT_MS", 20000), + bt_request_timeout: env_parsed("STREAM_SERVER_BT_REQUEST_TIMEOUT_MS", 10000), + bt_download_speed_soft_limit: env_parsed( + "STREAM_SERVER_BT_DOWNLOAD_SPEED_SOFT_LIMIT", + 0.0, + ), + bt_download_speed_hard_limit: env_parsed( + "STREAM_SERVER_BT_DOWNLOAD_SPEED_HARD_LIMIT", + 11.25 * 1024.0 * 1024.0, // ~90Mbps of a 100Mbps line, in bytes/sec + ), + bt_min_peers_for_stable: env_parsed("STREAM_SERVER_BT_MIN_PEERS_FOR_STABLE", 3), + bt_enable_dht: env_bool("STREAM_SERVER_BT_ENABLE_DHT", default_bt_enable_dht()), + bt_enable_pex: env_bool("STREAM_SERVER_BT_ENABLE_PEX", default_bt_enable_pex()), + bt_enable_lsd: env_bool("STREAM_SERVER_BT_ENABLE_LSD", default_bt_enable_lsd()), + bt_encryption_mode: env_encryption_mode( + "STREAM_SERVER_BT_ENCRYPTION_MODE", + TorrentEncryptionMode::default(), + ), + bt_anonymous_mode: env_bool( + "STREAM_SERVER_BT_ANONYMOUS_MODE", + default_bt_anonymous_mode(), + ), + bt_allow_multiple_connections_per_ip: env_bool( + "STREAM_SERVER_BT_ALLOW_MULTIPLE_CONNECTIONS_PER_IP", + true, + ), + bt_listen_interfaces: env_string( + "STREAM_SERVER_BT_LISTEN_INTERFACES", + default_bt_listen_interfaces(), + ), + bt_outgoing_interfaces: env_string("STREAM_SERVER_BT_OUTGOING_INTERFACES", String::new()), + bt_outgoing_port: env_parsed("STREAM_SERVER_BT_OUTGOING_PORT", 0), + bt_num_outgoing_ports: env_parsed("STREAM_SERVER_BT_NUM_OUTGOING_PORTS", 0), + bt_proxy_type: env_proxy_type("STREAM_SERVER_BT_PROXY_TYPE", TorrentProxyType::default()), + bt_proxy_host: env_string("STREAM_SERVER_BT_PROXY_HOST", String::new()), + bt_proxy_port: env_parsed("STREAM_SERVER_BT_PROXY_PORT", 0), + bt_proxy_username: env_string("STREAM_SERVER_BT_PROXY_USERNAME", String::new()), + bt_proxy_password: env_string("STREAM_SERVER_BT_PROXY_PASSWORD", String::new()), + bt_proxy_hostnames: env_bool( + "STREAM_SERVER_BT_PROXY_HOSTNAMES", + default_bt_proxy_hostnames(), + ), + bt_proxy_peer_connections: env_bool( + "STREAM_SERVER_BT_PROXY_PEER_CONNECTIONS", + default_bt_proxy_peer_connections(), + ), + bt_proxy_tracker_connections: env_bool( + "STREAM_SERVER_BT_PROXY_TRACKER_CONNECTIONS", + default_bt_proxy_tracker_connections(), + ), + bt_proxy_send_host_in_connect: env_bool( + "STREAM_SERVER_BT_PROXY_SEND_HOST_IN_CONNECT", + default_bt_proxy_send_host_in_connect(), + ), + bt_validate_https_trackers: env_bool( + "STREAM_SERVER_BT_VALIDATE_HTTPS_TRACKERS", + default_bt_validate_https_trackers(), + ), + bt_ssrf_mitigation: env_bool( + "STREAM_SERVER_BT_SSRF_MITIGATION", + default_bt_ssrf_mitigation(), + ), remote_https: None, transcode_profile: None, auto_update_enabled: default_auto_update_enabled(), @@ -420,7 +514,7 @@ impl Default for ServerSettings { cached_trackers: Vec::new(), trackers_last_updated: 0, trackers_source_url: default_trackers_url(), - seeding_enabled: default_seeding_enabled(), + seeding_enabled: env_bool("STREAM_SERVER_SEEDING_ENABLED", default_seeding_enabled()), } } }