Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95ce4edf9b | ||
|
|
536fef85a8 | ||
|
|
3c321b4036 | ||
|
|
6822ea7202 | ||
|
|
8a5223b960 | ||
|
|
db9461cf59 | ||
|
|
b0c067ed41 |
@@ -0,0 +1,58 @@
|
||||
#cloud-config
|
||||
# Gitea Actions runner on an Oracle Cloud Always Free Ampere (ARM) VM, shared by
|
||||
# every repo on the instance.
|
||||
#
|
||||
# Paste this into "Advanced options -> Management -> Initialization script" when
|
||||
# creating the instance (image: Canonical Ubuntu 24.04, shape: VM.Standard.A1.Flex).
|
||||
# Before pasting, replace REGISTRATION_TOKEN below with an instance-wide token:
|
||||
# Site Administration -> Actions -> Runners -> Create new runner
|
||||
# (a token from repo Settings would limit the runner to that one repo).
|
||||
#
|
||||
# The runner only makes outbound connections (to Gitea and container registries),
|
||||
# so the VM needs no inbound ports beyond SSH.
|
||||
#
|
||||
# Labels follow the instance convention (same set as the freebox ARM runner).
|
||||
# ubuntu-latest / ubuntu-22.04 are shared with the amd64 runner, so a job asking
|
||||
# for them may land on either architecture; ask for linux/arm64 or linux/amd64
|
||||
# when a job needs a specific one. SRF Play jobs run in the multi-arch
|
||||
# srfplay-builder image, so they work on both.
|
||||
|
||||
# Optional: a password for the "ubuntu" user, so you can log in through the Oracle
|
||||
# serial console ("Launch Cloud Shell connection") without an SSH key. Generate the
|
||||
# hash locally with: openssl passwd -6
|
||||
# then uncomment and paste it in. SSH still only accepts keys.
|
||||
#chpasswd:
|
||||
# expire: false
|
||||
# users:
|
||||
# - name: ubuntu
|
||||
# password: "$6$...paste the hash here..."
|
||||
|
||||
package_update: true
|
||||
packages:
|
||||
- docker.io
|
||||
- docker-compose-v2
|
||||
|
||||
write_files:
|
||||
- path: /opt/act_runner/docker-compose.yml
|
||||
permissions: "0600"
|
||||
content: |
|
||||
services:
|
||||
runner:
|
||||
image: gitea/act_runner:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
GITEA_INSTANCE_URL: https://gitea.tourolle.paris
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN: REGISTRATION_TOKEN
|
||||
GITEA_RUNNER_NAME: oracle-a1
|
||||
# Comma-separated with no spaces: label:docker://<default job image>
|
||||
GITEA_RUNNER_LABELS: "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest,ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04,linux-arm64:docker://gitea/runner-images:ubuntu-latest,self-hosted:docker://gitea/runner-images:ubuntu-latest,linux/arm64:docker://gitea/runner-images:ubuntu-latest"
|
||||
volumes:
|
||||
# Registration is stored here, so the token is only used once.
|
||||
- ./data:/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
runcmd:
|
||||
- systemctl enable --now docker
|
||||
- cd /opt/act_runner && docker compose up -d
|
||||
# Weekly clean-up so pulled images and build caches do not fill the boot volume.
|
||||
- echo '0 4 * * 0 root docker system prune -af --filter "until=168h" >/dev/null 2>&1' > /etc/cron.d/docker-prune
|
||||
@@ -15,7 +15,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: linux/amd64
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: gitea.tourolle.paris/dtourolle/srfplay-builder:latest
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
|
||||
jobs:
|
||||
nightly-build:
|
||||
runs-on: linux/amd64
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: gitea.tourolle.paris/dtourolle/srfplay-builder:latest
|
||||
|
||||
@@ -111,14 +111,14 @@ jobs:
|
||||
fi
|
||||
|
||||
# Upload one plugin zip per Jellyfin generation
|
||||
while IFS=$'\t' read -r _ _ ZIP _; do
|
||||
cut -f3 artifacts/builds.tsv | while read -r ZIP; do
|
||||
echo "Uploading ${ZIP}..."
|
||||
curl -f -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/zip" \
|
||||
--data-binary "@${ZIP}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=$(basename "${ZIP}")"
|
||||
done < artifacts/builds.tsv
|
||||
done
|
||||
|
||||
# Upload build.yaml
|
||||
echo "Uploading build.yaml..."
|
||||
|
||||
@@ -13,7 +13,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: linux/amd64
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: gitea.tourolle.paris/dtourolle/srfplay-builder:latest
|
||||
|
||||
@@ -91,14 +91,14 @@ jobs:
|
||||
fi
|
||||
|
||||
# Upload one plugin zip per Jellyfin generation
|
||||
while IFS=$'\t' read -r _ _ ZIP _; do
|
||||
cut -f3 artifacts/builds.tsv | while read -r ZIP; do
|
||||
echo "Uploading ${ZIP}..."
|
||||
curl -f -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/zip" \
|
||||
--data-binary "@${ZIP}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=$(basename "${ZIP}")"
|
||||
done < artifacts/builds.tsv
|
||||
done
|
||||
|
||||
# Upload build.yaml
|
||||
echo "Uploading build.yaml..."
|
||||
|
||||
@@ -17,7 +17,7 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: linux/amd64
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: gitea.tourolle.paris/dtourolle/srfplay-builder:latest
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@ obj/
|
||||
.idea/
|
||||
artifacts
|
||||
*.log
|
||||
.test-plugin/
|
||||
|
||||
+5
-2
@@ -1,7 +1,10 @@
|
||||
# SRFPlay Builder Image
|
||||
# Pre-built image with .NET SDK and JPRM for building Jellyfin plugins
|
||||
# Build: docker build -f Dockerfile.builder -t gitea.tourolle.paris/dtourolle/srfplay-builder:latest .
|
||||
# Push: docker push gitea.tourolle.paris/dtourolle/srfplay-builder:latest
|
||||
# Multi-arch (amd64 + arm64) so runners on ARM hosts such as Oracle Cloud's free
|
||||
# Ampere VMs can use it too; the plugin DLLs it produces are identical either way.
|
||||
# Build + push (needs a docker-container buildx builder and arm64 binfmt/QEMU):
|
||||
# docker buildx build -f Dockerfile.builder --platform linux/amd64,linux/arm64 \
|
||||
# -t gitea.tourolle.paris/dtourolle/srfplay-builder:latest --push .
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -303,8 +303,8 @@ public abstract class SrgChannelBase : IChannel, IHasCacheKey
|
||||
if (matchingEvent?.ValidFrom != null)
|
||||
{
|
||||
var eventTime = matchingEvent.ValidFrom.Value;
|
||||
item.Name = $"[{eventTime:dd.MM HH:mm}] {matchingEvent.Title}";
|
||||
item.PremiereDate = eventTime;
|
||||
item.Name = $"[{DisplayTime.From(eventTime):dd.MM HH:mm}] {matchingEvent.Title}";
|
||||
item.PremiereDate = eventTime.ToUniversalTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +169,14 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
/// </summary>
|
||||
public string RecordingOutputPath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time zone used for times the plugin writes as text: the start time in
|
||||
/// upcoming livestream names and the timestamp in recording file names. An IANA id such as
|
||||
/// "Europe/Zurich"; empty uses the server's time zone. Dates Jellyfin shows itself (e.g.
|
||||
/// PremiereDate) are stored as UTC and follow each viewer's own time zone regardless.
|
||||
/// </summary>
|
||||
public string DisplayTimeZone { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how many segments back from the live edge livestream playback should start.
|
||||
/// Injected as <c>#EXT-X-START:TIME-OFFSET=-(N * targetDuration)</c> into the live media
|
||||
|
||||
@@ -82,6 +82,11 @@
|
||||
<input id="RecordingOutputPath" name="RecordingOutputPath" type="text" is="emby-input" placeholder="e.g., /media/recordings/srf" />
|
||||
<div class="fieldDescription">Directory where sport livestream recordings will be saved (requires ffmpeg)</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="DisplayTimeZone">Display Time Zone</label>
|
||||
<input id="DisplayTimeZone" name="DisplayTimeZone" type="text" is="emby-input" placeholder="e.g., Europe/Zurich" />
|
||||
<div class="fieldDescription">Time zone for start times in upcoming livestream names and for recording file names. Leave empty to use the server's time zone.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="LiveStartSegmentsBack">Live Start Offset (segments)</label>
|
||||
<input id="LiveStartSegmentsBack" name="LiveStartSegmentsBack" type="number" is="emby-input" min="0" max="20" />
|
||||
@@ -200,6 +205,7 @@
|
||||
document.querySelector('#ProxyPassword').value = config.ProxyPassword || '';
|
||||
document.querySelector('#PublicServerUrl').value = config.PublicServerUrl || '';
|
||||
document.querySelector('#RecordingOutputPath').value = config.RecordingOutputPath || '';
|
||||
document.querySelector('#DisplayTimeZone').value = config.DisplayTimeZone || '';
|
||||
document.querySelector('#LiveStartSegmentsBack').value = config.LiveStartSegmentsBack != null ? config.LiveStartSegmentsBack : 3;
|
||||
document.querySelector('#CleanUpResumePoints').checked = config.CleanUpResumePoints !== false;
|
||||
document.querySelector('#ClearLiveStreamResumePoints').checked = config.ClearLiveStreamResumePoints !== false;
|
||||
@@ -231,6 +237,7 @@
|
||||
config.ProxyPassword = document.querySelector('#ProxyPassword').value;
|
||||
config.PublicServerUrl = document.querySelector('#PublicServerUrl').value;
|
||||
config.RecordingOutputPath = document.querySelector('#RecordingOutputPath').value;
|
||||
config.DisplayTimeZone = document.querySelector('#DisplayTimeZone').value.trim();
|
||||
config.LiveStartSegmentsBack = parseInt(document.querySelector('#LiveStartSegmentsBack').value) || 0;
|
||||
config.CleanUpResumePoints = document.querySelector('#CleanUpResumePoints').checked;
|
||||
config.ClearLiveStreamResumePoints = document.querySelector('#ClearLiveStreamResumePoints').checked;
|
||||
|
||||
@@ -451,7 +451,7 @@ public class RecordingService : IRecordingService, IDisposable
|
||||
|
||||
// Build output file path
|
||||
var safeTitle = SanitizeFileName(entry.Title);
|
||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HHmm", CultureInfo.InvariantCulture);
|
||||
var timestamp = DisplayTime.Now().ToString("yyyy-MM-dd_HHmm", CultureInfo.InvariantCulture);
|
||||
var outputPath = Path.Combine(GetRecordingOutputPath(), $"{safeTitle}_{timestamp}.mkv");
|
||||
entry.OutputPath = outputPath;
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Plugin.SRFPlay.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Converts times into the time zone the plugin writes into text (item names, file names).
|
||||
/// SRG sends times with an offset, and DateTime deserialisation turns them into server-local
|
||||
/// values, so without this the text follows whatever zone the server happens to run in.
|
||||
/// </summary>
|
||||
public static class DisplayTime
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a time to the configured display time zone (the server's if none is set).
|
||||
/// </summary>
|
||||
/// <param name="value">The time. Unspecified values are treated as server-local, like deserialised ones.</param>
|
||||
/// <returns>The same instant expressed in the display time zone.</returns>
|
||||
public static DateTime From(DateTime value)
|
||||
{
|
||||
return TimeZoneInfo.ConvertTimeFromUtc(value.ToUniversalTime(), GetZone());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current time in the display time zone.
|
||||
/// </summary>
|
||||
/// <returns>The current time in the display time zone.</returns>
|
||||
public static DateTime Now()
|
||||
{
|
||||
return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, GetZone());
|
||||
}
|
||||
|
||||
private static TimeZoneInfo GetZone()
|
||||
{
|
||||
var id = Plugin.Instance?.Configuration.DisplayTimeZone;
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
return TimeZoneInfo.Local;
|
||||
}
|
||||
|
||||
// An unknown id falls back to the server's zone rather than breaking listings.
|
||||
return TimeZoneInfo.TryFindSystemTimeZoneById(id.Trim(), out var zone) ? zone : TimeZoneInfo.Local;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,54 @@
|
||||
"category": "Live TV",
|
||||
"imageUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/raw/branch/master/assests/main%20logo.png",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.0.20260924.21612",
|
||||
"changelog": "Nightly build 1.0.20260924.21612 (3c321b4)",
|
||||
"targetAbi": "12.0.0.0",
|
||||
"sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/latest/srfplay_1.0.20260924.21612.zip",
|
||||
"checksum": "146824b350e6f10c0861dea198e5dd51",
|
||||
"timestamp": "2026-09-24T03:01:30Z"
|
||||
},
|
||||
{
|
||||
"version": "1.0.20260924.21611",
|
||||
"changelog": "Nightly build 1.0.20260924.21611 (3c321b4)",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/latest/srfplay_1.0.20260924.21611.zip",
|
||||
"checksum": "e63045bda702234285ee6d2f62034397",
|
||||
"timestamp": "2026-09-24T03:01:30Z"
|
||||
},
|
||||
{
|
||||
"version": "1.0.20260924.21609",
|
||||
"changelog": "Nightly build 1.0.20260924.21609 (3c321b4)",
|
||||
"targetAbi": "10.9.0.0",
|
||||
"sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/latest/srfplay_1.0.20260924.21609.zip",
|
||||
"checksum": "57592a884c09600d11f82702ac5d1805",
|
||||
"timestamp": "2026-09-24T03:01:30Z"
|
||||
},
|
||||
{
|
||||
"version": "1.0.20260924.21212",
|
||||
"changelog": "Nightly build 1.0.20260924.21212 (db9461c)",
|
||||
"targetAbi": "12.0.0.0",
|
||||
"sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/latest/srfplay_1.0.20260924.21212.zip",
|
||||
"checksum": "b497b4f8a9c5822909b6df0ed37b8b3d",
|
||||
"timestamp": "2026-09-24T01:14:34Z"
|
||||
},
|
||||
{
|
||||
"version": "1.0.20260924.21211",
|
||||
"changelog": "Nightly build 1.0.20260924.21211 (db9461c)",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/latest/srfplay_1.0.20260924.21211.zip",
|
||||
"checksum": "e3b98d2db0253dc42c3cc87645bffa0e",
|
||||
"timestamp": "2026-09-24T01:14:34Z"
|
||||
},
|
||||
{
|
||||
"version": "1.0.20260924.21209",
|
||||
"changelog": "Nightly build 1.0.20260924.21209 (db9461c)",
|
||||
"targetAbi": "10.9.0.0",
|
||||
"sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/latest/srfplay_1.0.20260924.21209.zip",
|
||||
"checksum": "1f4b3fe079d32a08968d7dfb99dc813f",
|
||||
"timestamp": "2026-09-24T01:14:34Z"
|
||||
},
|
||||
{
|
||||
"version": "1.0.20260908.201",
|
||||
"changelog": "Nightly build 1.0.20260908.201 (d0d5d06)",
|
||||
|
||||
@@ -8,6 +8,30 @@
|
||||
"category": "Live TV",
|
||||
"imageUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/raw/branch/master/assests/main%20logo.png",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.2.1.12",
|
||||
"changelog": "Release 1.2.1",
|
||||
"targetAbi": "12.0.0.0",
|
||||
"sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/v1.2.1/srfplay_1.2.1.12.zip",
|
||||
"checksum": "e7fe29977ed387f5f324b58aa40d8ebb",
|
||||
"timestamp": "2026-09-24T01:16:46Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.1.11",
|
||||
"changelog": "Release 1.2.1",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/v1.2.1/srfplay_1.2.1.11.zip",
|
||||
"checksum": "f69293b34c2e0e5fc5f2f2117ffb3ded",
|
||||
"timestamp": "2026-09-24T01:16:46Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.1.9",
|
||||
"changelog": "Release 1.2.1",
|
||||
"targetAbi": "10.9.0.0",
|
||||
"sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/v1.2.1/srfplay_1.2.1.9.zip",
|
||||
"checksum": "f18230d45e510cf60dce3badcd3f29a1",
|
||||
"timestamp": "2026-09-24T01:16:46Z"
|
||||
},
|
||||
{
|
||||
"version": "1.1.2",
|
||||
"changelog": "Release 1.1.2",
|
||||
|
||||
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