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 @@ + +