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:<tag>) so several can coexist. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,3 +4,4 @@ obj/
|
|||||||
.idea/
|
.idea/
|
||||||
artifacts
|
artifacts
|
||||||
*.log
|
*.log
|
||||||
|
.test-plugin/
|
||||||
|
|||||||
@@ -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 <config>/plugins.
|
||||||
|
COPY .test-plugin/SRFPlay /config/plugins/SRFPlay
|
||||||
Executable
+106
@@ -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" <<EOF
|
||||||
|
{
|
||||||
|
"guid": "$GUID",
|
||||||
|
"name": "SRF Play",
|
||||||
|
"version": "$VERSION",
|
||||||
|
"targetAbi": "$ABI",
|
||||||
|
"owner": "dtourolle",
|
||||||
|
"overview": "SRF Play plugin for Jellyfin (test build)",
|
||||||
|
"description": "Local test build of the SRF Play plugin.",
|
||||||
|
"category": "Live TV",
|
||||||
|
"status": "Active",
|
||||||
|
"autoUpdate": false
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ">> 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"
|
||||||
Reference in New Issue
Block a user