From 71180941e57cffdcf31f577fd105b1078acea75b Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Tue, 8 Sep 2026 18:48:02 +0200 Subject: [PATCH] Add Jellyfin 12.0 release build alongside 10.11 Multi-target the plugin against net9.0 (Jellyfin 10.11.x) and net10.0 (Jellyfin 12.0.x), with per-framework Jellyfin.Controller/Model references. Jellyfin 12 added a controlling-session parameter to ISessionManager.ReportCapabilities; an empty value skips the AssertCanControl check, which is what a server-side registration needs. Each release now ships two packages, jellylms__jf11.zip and jellylms__jf12.zip, with a manifest entry each. Jellyfin filters by targetAbi, so both can share a version number. Also corrects the manifest targetAbi for new releases from 10.10.0.0 to 10.11.0.0 - the plugin has referenced 10.11 packages since 1.0.0. jprm only reads ./build.yaml, so build-plugin.sh swaps targetAbi/framework per variant and restores the file afterwards. The builder image moves to the .NET 10 SDK, which targets both frameworks. CA1873 (new in that SDK) is disabled alongside CA1848, same rationale. Co-Authored-By: Claude Opus 5 (1M context) --- .gitea/workflows/build.yaml | 19 ++-- .gitea/workflows/release.yaml | 106 +++++++++--------- Dockerfile.builder | 2 +- .../Jellyfin.Plugin.JellyLMS.csproj | 21 +++- .../Services/LmsDeviceDiscoveryService.cs | 6 + README.md | 30 +++-- build-plugin.sh | 53 +++++++++ jellyfin.ruleset | 2 + 8 files changed, 161 insertions(+), 78 deletions(-) create mode 100755 build-plugin.sh diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 68990f0..2e9a99e 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -40,23 +40,20 @@ jobs: working-directory: build-${{ github.run_id }} run: dotnet build Jellyfin.Plugin.JellyLMS.sln --configuration Release --no-restore --no-self-contained /m:1 - - name: Build Jellyfin Plugin - id: jprm + - name: Build Jellyfin plugin packages working-directory: build-${{ github.run_id }} run: | - mkdir -p artifacts - jprm --verbosity=debug plugin build . - ARTIFACT=$(find . -name "*.zip" -type f -print -quit | sed 's|^\./||') - LATEST="artifacts/jellylms_latest.zip" - cp "${ARTIFACT}" "${LATEST}" - echo "artifact=${LATEST}" >> $GITHUB_OUTPUT - echo "Found artifact: ${ARTIFACT} -> ${LATEST}" + for VARIANT in jf11 jf12; do + ARTIFACT=$(./build-plugin.sh "${VARIANT}") + cp "${ARTIFACT}" "artifacts/jellylms_latest_${VARIANT}.zip" + echo "Built ${VARIANT}: ${ARTIFACT}" + done - - name: Upload build artifact + - name: Upload build artifacts uses: actions/upload-artifact@v3 with: name: jellylms-plugin - path: build-${{ github.run_id }}/${{ steps.jprm.outputs.artifact }} + path: build-${{ github.run_id }}/artifacts/jellylms_latest_*.zip retention-days: 30 if-no-files-found: error diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 9485899..1bcb229 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -35,13 +35,6 @@ jobs: echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT echo "Building version: ${VERSION}" - - name: Update build.yaml with version - working-directory: release-${{ github.run_id }} - run: | - VERSION="${{ steps.get_version.outputs.version_number }}" - sed -i "s/^version:.*/version: \"${VERSION}\"/" build.yaml - cat build.yaml - - name: Cache NuGet packages uses: actions/cache@v3 with: @@ -57,17 +50,18 @@ jobs: working-directory: release-${{ github.run_id }} run: dotnet build Jellyfin.Plugin.JellyLMS.sln --configuration Release --no-restore --no-self-contained /m:1 - - name: Build Jellyfin Plugin + - name: Build Jellyfin plugin packages id: jprm working-directory: release-${{ github.run_id }} run: | - mkdir -p artifacts - jprm --verbosity=debug plugin build ./ - ARTIFACT=$(find . -name "*.zip" -type f -print -quit | sed 's|^\./||') - ARTIFACT_NAME=$(basename "${ARTIFACT}") - echo "artifact=${ARTIFACT}" >> $GITHUB_OUTPUT - echo "artifact_name=${ARTIFACT_NAME}" >> $GITHUB_OUTPUT - echo "Found artifact: ${ARTIFACT}" + VERSION="${{ steps.get_version.outputs.version_number }}" + for VARIANT in jf11 jf12; do + ARTIFACT=$(./build-plugin.sh "${VARIANT}" "${VERSION}") + echo "${VARIANT}_artifact=${ARTIFACT}" >> $GITHUB_OUTPUT + echo "${VARIANT}_artifact_name=$(basename "${ARTIFACT}")" >> $GITHUB_OUTPUT + echo "${VARIANT}_checksum=$(md5sum "${ARTIFACT}" | awk '{print $1}')" >> $GITHUB_OUTPUT + echo "Built ${VARIANT}: ${ARTIFACT}" + done - name: Create Release working-directory: release-${{ github.run_id }} @@ -81,11 +75,18 @@ jobs: # Create release using Gitea API VERSION="${{ steps.get_version.outputs.version }}" + RELEASE_BODY=$(printf '%s\n' \ + "JellyLMS Jellyfin Plugin ${VERSION}." \ + "" \ + "Pick the package matching your server:" \ + "" \ + "- \`${{ steps.jprm.outputs.jf11_artifact_name }}\` - Jellyfin 10.11.x" \ + "- \`${{ steps.jprm.outputs.jf12_artifact_name }}\` - Jellyfin 12.0.x") RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases" \ - -d "$(jq -n --arg tag "$VERSION" --arg name "Release $VERSION" --arg body "JellyLMS Jellyfin Plugin ${VERSION}. See attached files for plugin installation." '{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')") + -d "$(jq -n --arg tag "$VERSION" --arg name "Release $VERSION" --arg body "$RELEASE_BODY" '{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')") HTTP_CODE=$(echo "$RESPONSE" | tail -n1) BODY=$(echo "$RESPONSE" | sed '$d') @@ -99,33 +100,21 @@ jobs: exit 1 fi - # Upload plugin artifact - echo "Uploading plugin artifact..." - curl -f -X POST \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Content-Type: application/zip" \ - --data-binary "@${{ steps.jprm.outputs.artifact }}" \ - "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=${{ steps.jprm.outputs.artifact_name }}" - - # Upload build.yaml - echo "Uploading build.yaml..." - curl -f -X POST \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Content-Type: application/x-yaml" \ - --data-binary "@build.yaml" \ - "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=build.yaml" + # Upload plugin artifacts (one per supported Jellyfin generation) + for ASSET in \ + "${{ steps.jprm.outputs.jf11_artifact }}" \ + "${{ steps.jprm.outputs.jf12_artifact }}"; do + echo "Uploading $(basename "${ASSET}")..." + curl -f -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/zip" \ + --data-binary "@${ASSET}" \ + "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=$(basename "${ASSET}")" + done echo "Release created successfully!" echo "View at: ${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/releases/tag/${{ steps.get_version.outputs.version }}" - - name: Calculate checksum - id: checksum - working-directory: release-${{ github.run_id }} - run: | - CHECKSUM=$(md5sum "${{ steps.jprm.outputs.artifact }}" | awk '{print $1}') - echo "checksum=${CHECKSUM}" >> $GITHUB_OUTPUT - echo "MD5 checksum: ${CHECKSUM}" - - name: Update manifest.json working-directory: release-${{ github.run_id }} run: | @@ -135,30 +124,35 @@ jobs: git checkout master VERSION="${{ steps.get_version.outputs.version_number }}" - CHECKSUM="${{ steps.checksum.outputs.checksum }}" TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - ARTIFACT_NAME="${{ steps.jprm.outputs.artifact_name }}" REPO_OWNER="${{ github.repository_owner }}" REPO_NAME="${{ github.event.repository.name }}" GITEA_URL="${{ github.server_url }}" - DOWNLOAD_URL="${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/releases/download/${{ steps.get_version.outputs.version }}/${ARTIFACT_NAME}" + RELEASE_URL="${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/releases/download/${{ steps.get_version.outputs.version }}" - # Create the new version entry - NEW_VERSION=$(cat < manifest.tmp.json + mv manifest.tmp.json manifest.json } - EOF - ) - # Prepend new version to the versions array in manifest.json - jq --argjson newver "${NEW_VERSION}" '.[0].versions = [$newver] + .[0].versions' manifest.json > manifest.tmp.json - mv manifest.tmp.json manifest.json + add_version "10.11.0.0" "${{ steps.jprm.outputs.jf11_artifact_name }}" "${{ steps.jprm.outputs.jf11_checksum }}" + add_version "12.0.0.0" "${{ steps.jprm.outputs.jf12_artifact_name }}" "${{ steps.jprm.outputs.jf12_checksum }}" echo "Updated manifest.json:" cat manifest.json diff --git a/Dockerfile.builder b/Dockerfile.builder index 6179bdb..d17ed0f 100644 --- a/Dockerfile.builder +++ b/Dockerfile.builder @@ -3,7 +3,7 @@ # Build: docker build -f Dockerfile.builder -t gitea.tourolle.paris/dtourolle/jellylms-builder:latest . # Push: docker push gitea.tourolle.paris/dtourolle/jellylms-builder:latest -FROM mcr.microsoft.com/dotnet/sdk:9.0 +FROM mcr.microsoft.com/dotnet/sdk:10.0 RUN apt-get update && apt-get install -y \ python3 \ diff --git a/Jellyfin.Plugin.JellyLMS/Jellyfin.Plugin.JellyLMS.csproj b/Jellyfin.Plugin.JellyLMS/Jellyfin.Plugin.JellyLMS.csproj index dece144..7a2d502 100644 --- a/Jellyfin.Plugin.JellyLMS/Jellyfin.Plugin.JellyLMS.csproj +++ b/Jellyfin.Plugin.JellyLMS/Jellyfin.Plugin.JellyLMS.csproj @@ -1,7 +1,13 @@ + - net9.0 + net9.0;net10.0 Jellyfin.Plugin.JellyLMS true true @@ -10,8 +16,8 @@ ../jellyfin.ruleset - - + + runtime @@ -19,6 +25,15 @@ + + + runtime + + + runtime + + + diff --git a/Jellyfin.Plugin.JellyLMS/Services/LmsDeviceDiscoveryService.cs b/Jellyfin.Plugin.JellyLMS/Services/LmsDeviceDiscoveryService.cs index 8ba0193..74a31a6 100644 --- a/Jellyfin.Plugin.JellyLMS/Services/LmsDeviceDiscoveryService.cs +++ b/Jellyfin.Plugin.JellyLMS/Services/LmsDeviceDiscoveryService.cs @@ -211,7 +211,13 @@ public class LmsDeviceDiscoveryService : IHostedService, IDisposable SupportsPersistentIdentifier = true }; +#if NET10_0_OR_GREATER + // Jellyfin 12 added a controlling session parameter; an empty value skips the + // "can control" assertion, which is what we want for a server-side registration. + sessionManager.ReportCapabilities(string.Empty, session.Id, capabilities); +#else sessionManager.ReportCapabilities(session.Id, capabilities); +#endif // Track this device _registeredDeviceIds[player.MacAddress] = deviceId; diff --git a/README.md b/README.md index 5e5b2b5..153acf1 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,19 @@ Create and manage synchronized playback groups for multi-room audio: ## Requirements -- Jellyfin Server 10.10.0 or later -- .NET 9.0 Runtime +- Jellyfin Server 10.11.x or 12.0.x - Logitech Media Server (LMS) with JSON-RPC API enabled (default on port 9000) +Each release ships two packages, one per Jellyfin generation: + +| Package | Jellyfin | targetAbi | Runtime | +| --- | --- | --- | --- | +| `jellylms__jf11.zip` | 10.11.x | `10.11.0.0` | .NET 9.0 | +| `jellylms__jf12.zip` | 12.0.x | `12.0.0.0` | .NET 10.0 | + +If you install through the plugin repository, Jellyfin picks the matching +package automatically. For manual installs, download the one for your server. + ## Playback Architecture JellyLMS uses Jellyfin's native "Play On" (cast) interface to control LMS players. When you select an LMS player from Jellyfin's cast menu, playback is managed through a robust state machine that ensures reliable operation. @@ -151,13 +160,20 @@ The state machine includes automatic retry with exponential backoff: git clone https://gitea.tourolle.paris/dtourolle/jellyLMS.git cd jellyLMS -# Build +# Build both target frameworks dotnet build Jellyfin.Plugin.JellyLMS.sln -c Release -# The DLL will be in: -# Jellyfin.Plugin.JellyLMS/bin/Release/net9.0/ +# The DLLs will be in: +# Jellyfin.Plugin.JellyLMS/bin/Release/net9.0/ (Jellyfin 10.11.x) +# Jellyfin.Plugin.JellyLMS/bin/Release/net10.0/ (Jellyfin 12.0.x) + +# Or produce installable plugin packages (requires jprm) +./build-plugin.sh jf11 +./build-plugin.sh jf12 ``` +Building requires the .NET 10 SDK, which can target both `net9.0` and `net10.0`. + ## Configuration 1. Navigate to Jellyfin Dashboard → Plugins → JellyLMS @@ -293,8 +309,8 @@ Jellyfin.Plugin.JellyLMS/ ### Building for Development ```bash -# Build in debug mode -dotnet build Jellyfin.Plugin.JellyLMS.sln +# Build in debug mode (use -f net10.0 for a Jellyfin 12 server) +dotnet build Jellyfin.Plugin.JellyLMS.sln -f net9.0 # Copy to Jellyfin plugins directory cp Jellyfin.Plugin.JellyLMS/bin/Debug/net9.0/Jellyfin.Plugin.JellyLMS.dll \ diff --git a/build-plugin.sh b/build-plugin.sh new file mode 100755 index 0000000..b7eabe3 --- /dev/null +++ b/build-plugin.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# +# Package the JellyLMS plugin for one Jellyfin server generation. +# +# ./build-plugin.sh jf11 [version] -> Jellyfin 10.11.x, net9.0 +# ./build-plugin.sh jf12 [version] -> Jellyfin 12.0.x, net10.0 +# +# jprm always reads ./build.yaml, so this temporarily rewrites the targetAbi and +# framework fields for the requested variant and restores the file afterwards. +# The resulting zip is written to artifacts/ with a variant suffix, and its path +# is printed on stdout. +set -euo pipefail + +VARIANT="${1:-}" +VERSION="${2:-}" + +case "${VARIANT}" in + jf11) TARGET_ABI="10.11.0.0"; FRAMEWORK="net9.0" ;; + jf12) TARGET_ABI="12.0.0.0"; FRAMEWORK="net10.0" ;; + *) echo "usage: $0 [version]" >&2; exit 1 ;; +esac + +cd "$(dirname "$0")" + +if [ -z "${VERSION}" ]; then + VERSION=$(sed -n 's/^version:[[:space:]]*"\{0,1\}\([^"]*\)"\{0,1\}[[:space:]]*$/\1/p' build.yaml) +fi + +# jprm normalises the version to four components for the artifact name. +FULL_VERSION="${VERSION}" +while [ "$(printf '%s' "${FULL_VERSION}" | tr -cd '.' | wc -c)" -lt 3 ]; do + FULL_VERSION="${FULL_VERSION}.0" +done + +BACKUP=$(mktemp) +cp build.yaml "${BACKUP}" +trap 'cp "${BACKUP}" build.yaml; rm -f "${BACKUP}"' EXIT + +sed -i "s/^version:.*/version: \"${VERSION}\"/" build.yaml +sed -i "s/^targetAbi:.*/targetAbi: \"${TARGET_ABI}\"/" build.yaml +sed -i "s/^framework:.*/framework: \"${FRAMEWORK}\"/" build.yaml + +echo "Building ${VARIANT}: targetAbi=${TARGET_ABI} framework=${FRAMEWORK} version=${VERSION}" >&2 + +mkdir -p "artifacts/${VARIANT}" +jprm --verbosity=debug plugin build ./ --output "artifacts/${VARIANT}" >&2 + +SRC="artifacts/${VARIANT}/jellylms_${FULL_VERSION}.zip" +DEST="artifacts/jellylms_${FULL_VERSION}_${VARIANT}.zip" +mv "${SRC}" "${DEST}" +rm -rf "artifacts/${VARIANT}" + +echo "${DEST}" diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 2297279..207126a 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -100,6 +100,8 @@ + +