From b0c067ed41524e7b23fc55d538916f8e31117d10 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Thu, 24 Sep 2026 01:44:34 +0200 Subject: [PATCH] Add a Docker test harness for any Jellyfin version test-image.sh publishes the plugin on the host, bakes it into a stock jellyfin/jellyfin image via Dockerfile.test, and runs it on :8096, with optional egress through a LAN gateway (DOCKER_GATEWAY, e.g. the Swiss VPN box) for geo-blocked streams. JELLYFIN_TAG (default 12) selects the image and the matching plugin build: 10.9/10.10 -> net8.0, 10.11 -> net9.0, 12 -> net10.0, with the generated meta.json targetAbi to match. Images are tagged per version (srfplay-test:) so several can coexist. Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitignore | 1 + Dockerfile.test | 23 +++++++++++ test-image.sh | 106 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 Dockerfile.test create mode 100755 test-image.sh diff --git a/.gitignore b/.gitignore index 7c39d6b..11e5a79 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ obj/ .idea/ artifacts *.log +.test-plugin/ diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 0000000..743cd27 --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,23 @@ +# Test image: stock Jellyfin with a prebuilt SRF Play plugin baked in. +# +# This does NOT build the plugin — build it first on the host (fast, warm +# NuGet cache), then this image is just one small COPY on top of stock +# Jellyfin, so it builds in seconds. +# +# ./test-image.sh # builds plugin + image, runs it +# +# or manually: +# dotnet publish Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj \ +# -c Release -f net10.0 --no-self-contained -o ./.test-plugin/SRFPlay +# docker build -f Dockerfile.test --build-arg JELLYFIN_TAG=12 -t srfplay-test . +# docker run --rm -p 8096:8096 srfplay-test +# +# JELLYFIN_TAG must match the framework the plugin was published for; +# test-image.sh picks both from one setting. + +ARG JELLYFIN_TAG=12 +FROM jellyfin/jellyfin:${JELLYFIN_TAG} + +# .test-plugin/SRFPlay is produced by test-image.sh and contains the plugin +# DLL plus a generated meta.json. Jellyfin loads from /plugins. +COPY .test-plugin/SRFPlay /config/plugins/SRFPlay diff --git a/test-image.sh b/test-image.sh new file mode 100755 index 0000000..31ff787 --- /dev/null +++ b/test-image.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Build the SRF Play plugin from the local checkout, bake it into a stock +# Jellyfin image, and run it on http://localhost:8096. +# +# ./test-image.sh build + run +# ./test-image.sh build build image only (no run) +# +# JELLYFIN_TAG picks the Jellyfin image (default 12) and, with it, the matching +# plugin build (see the TargetFrameworks comment in the plugin csproj): +# +# JELLYFIN_TAG=10.11.0 ./test-image.sh +set -euo pipefail + +cd "$(dirname "$0")" + +JELLYFIN_TAG="${JELLYFIN_TAG:-12}" +PLUGIN_DIR=".test-plugin/SRFPlay" +IMAGE="srfplay-test:${JELLYFIN_TAG}" + +case "$JELLYFIN_TAG" in + 10.9*|10.10*) FRAMEWORK="net8.0"; ABI="10.9.0.0" ;; + 10.11*) FRAMEWORK="net9.0"; ABI="10.11.0.0" ;; + *) FRAMEWORK="net10.0"; ABI="12.0.0.0" ;; +esac + +echo ">> Publishing plugin for Jellyfin ${JELLYFIN_TAG} (${FRAMEWORK}, host build, warm cache)..." +rm -rf .test-plugin +dotnet publish Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj \ + --configuration Release --framework "$FRAMEWORK" --no-self-contained -o /tmp/srfplay-publish >/dev/null + +mkdir -p "$PLUGIN_DIR" +cp /tmp/srfplay-publish/Jellyfin.Plugin.SRFPlay.dll "$PLUGIN_DIR/" + +# Generate the meta.json Jellyfin needs to load the plugin, from build.yaml. +# targetAbi follows the framework chosen above rather than build.yaml. +GUID=$(grep -E '^guid:' build.yaml | sed -E 's/^guid:[[:space:]]*"?([^"]+)"?.*/\1/') +VERSION=$(grep -E '^version:' build.yaml | sed -E 's/^version:[[:space:]]*"?([^"]+)"?.*/\1/') + +cat > "$PLUGIN_DIR/meta.json" <> Building image $IMAGE (stock Jellyfin + plugin COPY)..." +docker build -f Dockerfile.test --build-arg JELLYFIN_TAG="$JELLYFIN_TAG" -t "$IMAGE" . + +if [ "${1:-run}" = "build" ]; then + echo ">> Built $IMAGE. Run with: docker run --rm -p 8096:8096 $IMAGE" + exit 0 +fi + +# --- Optional: route the container's egress via a LAN gateway ---------------- +# Set DOCKER_GATEWAY to a LAN gateway IP (e.g. the Swiss VPN box 192.168.1.52) +# to send this container's outbound traffic through it. Needs sudo once to add +# host policy-route + masquerade; both are removed automatically on exit. +# +# DOCKER_GATEWAY=192.168.1.52 ./test-image.sh +# +# When unset, the container uses Docker's normal default route (your ISP). + +TEST_NET="srfplay-testnet" +TEST_SUBNET="172.31.250.0/24" +TEST_TABLE="250" # dedicated rt table for this subnet +# The uplink is the interface that actually reaches the gateway (the LAN NIC, +# e.g. wlan0) — NOT the host default route, which a VPN killswitch may point +# at the tunnel. Derive it from the route to DOCKER_GATEWAY when set. +if [ -n "${DOCKER_GATEWAY:-}" ]; then + UPLINK_IF="$(ip route get "$DOCKER_GATEWAY" 2>/dev/null | grep -oE 'dev [^ ]+' | awk '{print $2}')" +fi + +cleanup() { + echo ">> Cleaning up..." + docker rm -f srfplay-test-run >/dev/null 2>&1 || true + if [ -n "${DOCKER_GATEWAY:-}" ]; then + sudo ip rule del from "$TEST_SUBNET" table "$TEST_TABLE" 2>/dev/null || true + sudo ip route flush table "$TEST_TABLE" 2>/dev/null || true + sudo iptables -t nat -D POSTROUTING -s "$TEST_SUBNET" -o "$UPLINK_IF" -j MASQUERADE 2>/dev/null || true + fi + docker network rm "$TEST_NET" >/dev/null 2>&1 || true +} +trap cleanup EXIT INT TERM + +docker network rm "$TEST_NET" >/dev/null 2>&1 || true +docker network create --subnet "$TEST_SUBNET" "$TEST_NET" >/dev/null + +if [ -n "${DOCKER_GATEWAY:-}" ]; then + echo ">> Routing container egress via gateway $DOCKER_GATEWAY (uplink $UPLINK_IF)..." + echo " (sudo needed once to install host routes; removed on exit)" + sudo ip route replace default via "$DOCKER_GATEWAY" dev "$UPLINK_IF" table "$TEST_TABLE" + sudo ip rule add from "$TEST_SUBNET" table "$TEST_TABLE" 2>/dev/null || true + sudo iptables -t nat -C POSTROUTING -s "$TEST_SUBNET" -o "$UPLINK_IF" -j MASQUERADE 2>/dev/null \ + || sudo iptables -t nat -A POSTROUTING -s "$TEST_SUBNET" -o "$UPLINK_IF" -j MASQUERADE +fi + +echo ">> Starting Jellyfin on http://localhost:8096 (Ctrl-C to stop)..." +docker run --rm --name srfplay-test-run --network "$TEST_NET" -p 8096:8096 "$IMAGE"