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_<version>_jf11.zip and jellylms_<version>_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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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..."
|
||||
# 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 "@${{ 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"
|
||||
--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 <<EOF
|
||||
{
|
||||
"version": "${VERSION}",
|
||||
"changelog": "Release ${VERSION}",
|
||||
"targetAbi": "10.10.0.0",
|
||||
"sourceUrl": "${DOWNLOAD_URL}",
|
||||
"checksum": "${CHECKSUM}",
|
||||
"timestamp": "${TIMESTAMP}"
|
||||
}
|
||||
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
|
||||
# One manifest entry per Jellyfin generation. Jellyfin filters the list by
|
||||
# targetAbi, so both entries can share the same version number.
|
||||
add_version() {
|
||||
local abi="$1" artifact_name="$2" checksum="$3"
|
||||
jq \
|
||||
--arg version "${VERSION}" \
|
||||
--arg abi "${abi}" \
|
||||
--arg url "${RELEASE_URL}/${artifact_name}" \
|
||||
--arg checksum "${checksum}" \
|
||||
--arg timestamp "${TIMESTAMP}" \
|
||||
'.[0].versions = [{
|
||||
version: $version,
|
||||
changelog: "Release \($version)",
|
||||
targetAbi: $abi,
|
||||
sourceUrl: $url,
|
||||
checksum: $checksum,
|
||||
timestamp: $timestamp
|
||||
}] + .[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
|
||||
|
||||
+1
-1
@@ -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 \
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!--
|
||||
Multi-targeted against the two supported Jellyfin server generations:
|
||||
net9.0 -> Jellyfin 10.11.x (targetAbi 10.11.0.0)
|
||||
net10.0 -> Jellyfin 12.0.x (targetAbi 12.0.0.0)
|
||||
jprm builds one framework at a time (see build.yaml / build.jf12.yaml).
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>
|
||||
<RootNamespace>Jellyfin.Plugin.JellyLMS</RootNamespace>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
@@ -10,7 +16,7 @@
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.11.0">
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</PackageReference>
|
||||
@@ -19,6 +25,15 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
|
||||
<PackageReference Include="Jellyfin.Controller" Version="12.0.0">
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Jellyfin.Model" Version="12.0.0">
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="All" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_<version>_jf11.zip` | 10.11.x | `10.11.0.0` | .NET 9.0 |
|
||||
| `jellylms_<version>_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 \
|
||||
|
||||
Executable
+53
@@ -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 <jf11|jf12> [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}"
|
||||
@@ -100,6 +100,8 @@
|
||||
<Rule Id="CA1308" Action="None" />
|
||||
<!-- disable warning CA1848: Use the LoggerMessage delegates -->
|
||||
<Rule Id="CA1848" Action="None" />
|
||||
<!-- disable warning CA1873: Evaluation of logging argument may be expensive (same rationale as CA1848) -->
|
||||
<Rule Id="CA1873" Action="None" />
|
||||
<!-- disable warning CA2101: Specify marshaling for P/Invoke string arguments -->
|
||||
<Rule Id="CA2101" Action="None" />
|
||||
<!-- disable warning CA2234: Pass System.Uri objects instead of strings -->
|
||||
|
||||
Reference in New Issue
Block a user