From 582c94f5f92248a548c5a003a48e47aaac56b112 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Thu, 24 Sep 2026 01:42:38 +0200 Subject: [PATCH] Build one plugin per Jellyfin generation (10.9, 10.11, 12) The single net8.0 build compiled against Jellyfin 10.9.11 still loads on 10.11 and 12, so channels and browsing keep working, but 10.11 changed APIs in ways that only fail at runtime. A binary audit of the DLL against the real server assemblies found 9 broken references on both 10.11 and 12 (none on 10.9/10.10): - TaskTriggerInfo.Type became an enum. GetDefaultTriggers throws, so Jellyfin silently gives every SRF task a 24h fallback trigger. The recording scheduler therefore ran about once a day and scheduled recordings never started. - ILibraryManager.GetItemList changed its return type: the expiration check crashed on every run. - Jellyfin.Data.Entities.User moved (IUserManager.Users, GetUserData, SaveUserData, PlaybackProgressEventArgs.Users): resume cleanup returned 500 and the playback-stop guard threw on every stop. Jellyfin 12 also rejects the legacy X-Emby-Token header with an empty 401, which the recordings UI surfaced as "Unexpected end of JSON input". The pages now send Authorization: MediaBrowser Token="...", verified on 10.9, 10.11 and 12. The plugin now multi-targets net8.0/net9.0/net10.0 against Jellyfin 10.9.11/10.11.0/12.0.0 (the oldest package of each line, so each DLL loads on every patch release of it), with JELLYFIN_10_11_OR_GREATER and JELLYFIN_12_OR_GREATER for the differences. Trigger construction moves into Utilities/TaskTriggers so the version switch lives in one place. .gitea/scripts/build-plugins.sh builds one zip per generation. jprm has no targetAbi flag, so it rewrites build.yaml per flavour and restores it. Jellyfin installs the highest version whose targetAbi it satisfies, so the flavour code goes into the last version segment: release v1.2.0 -> 1.2.0.9 / 1.2.0.11 / 1.2.0.12 nightly -> 1.0..09 / 11 / 12 The PR, nightly and release workflows use the script and publish one manifest entry per zip. Verified end to end against a three-entry manifest: 10.11 installs the .11 build, 12.1 installs the .12 build. The existing .NET 10 builder image builds all three unchanged. Supporting changes: the test project multi-targets too, because jprm publishes the whole solution for one framework at a time, and it drops its Microsoft.Extensions.Logging 8.0.1 pin, which is a NU1605 downgrade under the newer Jellyfin packages. CA1873 (new in the .NET 10 analyzers) is silenced beside CA1848. The release manifest step checks out origin/master like the nightly does since 7e1973a. Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitea/scripts/build-plugins.sh | 62 ++++++++++++++ .gitea/workflows/build.yaml | 23 ++---- .gitea/workflows/latest.yaml | 82 +++++++------------ .gitea/workflows/release.yaml | 76 +++++++---------- .../Jellyfin.Plugin.SRFPlay.Tests.csproj | 4 +- .../Configuration/configPage.html | 4 +- .../Configuration/recordingPage.html | 6 +- .../Jellyfin.Plugin.SRFPlay.csproj | 35 +++++++- .../ScheduledTasks/ContentRefreshTask.cs | 6 +- .../ScheduledTasks/ExpirationCheckTask.cs | 13 +-- .../ScheduledTasks/RecordingSchedulerTask.cs | 7 +- .../ScheduledTasks/ResumeCleanupTask.cs | 7 +- .../Interfaces/IResumeCleanupService.cs | 4 + .../Services/ResumeCleanupService.cs | 8 ++ .../Utilities/TaskTriggers.cs | 49 +++++++++++ README.md | 15 +++- jellyfin.ruleset | 2 + 17 files changed, 251 insertions(+), 152 deletions(-) create mode 100755 .gitea/scripts/build-plugins.sh create mode 100644 Jellyfin.Plugin.SRFPlay/Utilities/TaskTriggers.cs diff --git a/.gitea/scripts/build-plugins.sh b/.gitea/scripts/build-plugins.sh new file mode 100755 index 0000000..c28c883 --- /dev/null +++ b/.gitea/scripts/build-plugins.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Build one plugin zip per Jellyfin generation into ./artifacts. +# +# Jellyfin binds a plugin to the exact API it was compiled against, and 10.11 changed +# signatures that still compile but fail at runtime. So each generation gets its own DLL +# (see the TargetFrameworks comment in Jellyfin.Plugin.SRFPlay.csproj) and its own +# manifest entry. Jellyfin installs the highest version whose targetAbi is <= the server +# version, so the flavour code is folded into the version to keep them apart: +# +# net8.0 -> targetAbi 10.9.0.0 (Jellyfin 10.9 / 10.10) code 9 +# net9.0 -> targetAbi 10.11.0.0 (Jellyfin 10.11) code 11 +# net10.0 -> targetAbi 12.0.0.0 (Jellyfin 12) code 12 +# +# Usage: build-plugins.sh +# is a printf format that receives the flavour code, e.g. +# "1.2.0.%d" -> 1.2.0.9, 1.2.0.11, 1.2.0.12 (release) +# "1.0.20260924.204%02d" -> 1.0.20260924.20409, ...20411, ...20412 (nightly) +# +# Writes artifacts/builds.tsv with one line per zip: version, targetAbi, zip path, md5. +set -euo pipefail + +FORMAT="$1" + +FLAVOURS=( + "net8.0 10.9.0.0 9" + "net9.0 10.11.0.0 11" + "net10.0 12.0.0.0 12" +) + +mkdir -p artifacts +: > artifacts/builds.tsv + +# jprm has no targetAbi flag and reads it from build.yaml, so rewrite it per flavour. +cp build.yaml build.yaml.orig +trap 'mv -f build.yaml.orig build.yaml' EXIT + +for flavour in "${FLAVOURS[@]}"; do + read -r framework abi code <<< "$flavour" + # shellcheck disable=SC2059 # the format is the argument by design + version=$(printf "$FORMAT" "$code") + + cp build.yaml.orig build.yaml + sed -i \ + -e "s/^version:.*/version: \"${version}\"/" \ + -e "s/^targetAbi:.*/targetAbi: \"${abi}\"/" \ + -e "s/^framework:.*/framework: \"${framework}\"/" \ + -e "s/^dotnet_framework:.*/dotnet_framework: \"${framework}\"/" \ + build.yaml + + echo ">> ${version}: ${framework}, targetAbi ${abi}" + jprm --verbosity=debug plugin build . --dotnet-framework "${framework}" --version "${version}" + + zip="artifacts/srfplay_${version}.zip" + if [ ! -f "${zip}" ]; then + echo "jprm did not produce ${zip}" >&2 + exit 1 + fi + + printf '%s\t%s\t%s\t%s\n' "${version}" "${abi}" "${zip}" "$(md5sum "${zip}" | awk '{print $1}')" >> artifacts/builds.tsv +done + +cat artifacts/builds.tsv diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 5f1183e..739469c 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -51,14 +51,10 @@ jobs: LABEL="master" fi echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "format=${VERSION}%02d" >> $GITHUB_OUTPUT echo "label=${LABEL}" >> $GITHUB_OUTPUT echo "Build version: ${VERSION} (${LABEL})" - - name: Set build version - working-directory: build-${{ github.run_id }} - run: | - sed -i "s/^version:.*/version: \"${{ steps.version.outputs.version }}\"/" build.yaml - - name: Build solution working-directory: build-${{ github.run_id }} run: dotnet build Jellyfin.Plugin.SRFPlay.sln --configuration Release --no-restore --no-self-contained /m:1 @@ -67,23 +63,16 @@ jobs: working-directory: build-${{ github.run_id }} run: dotnet test Jellyfin.Plugin.SRFPlay.sln --no-build --configuration Release --verbosity normal - - name: Build Jellyfin Plugin - id: jprm + - name: Build Jellyfin Plugins 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/srfplay_latest.zip" - cp "${ARTIFACT}" "${LATEST}" - echo "artifact=${LATEST}" >> $GITHUB_OUTPUT - echo "Found artifact: ${ARTIFACT} -> ${LATEST}" + # One zip per Jellyfin generation (see .gitea/scripts/build-plugins.sh). + run: .gitea/scripts/build-plugins.sh "${{ steps.version.outputs.format }}" - - name: Upload build artifact + - name: Upload build artifacts uses: actions/upload-artifact@v3 with: name: srfplay-${{ steps.version.outputs.label }}-${{ steps.version.outputs.version }} - path: build-${{ github.run_id }}/${{ steps.jprm.outputs.artifact }} + path: build-${{ github.run_id }}/artifacts/*.zip retention-days: 30 if-no-files-found: error diff --git a/.gitea/workflows/latest.yaml b/.gitea/workflows/latest.yaml index 430dc43..c51e8bb 100644 --- a/.gitea/workflows/latest.yaml +++ b/.gitea/workflows/latest.yaml @@ -27,18 +27,15 @@ jobs: # Date-based nightly version: 1.0.. # The run_number suffix keeps multiple builds on the same day # monotonically increasing so Jellyfin always offers the newest. + # Each Jellyfin generation gets its own build, so the last segment + # is <2-digit flavour code>, e.g. run 204 -> 20409/20411/20412 + # (see .gitea/scripts/build-plugins.sh). DATE=$(date -u +"%Y%m%d") VERSION="1.0.${DATE}.${{ github.run_number }}" echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "format=${VERSION}%02d" >> $GITHUB_OUTPUT echo "Nightly version: ${VERSION}" - - name: Set build version - working-directory: build-${{ github.run_id }} - run: | - VERSION="${{ steps.version.outputs.version }}" - sed -i "s/^version:.*/version: \"${VERSION}\"/" build.yaml - cat build.yaml - - name: Restore dependencies working-directory: build-${{ github.run_id }} run: dotnet restore Jellyfin.Plugin.SRFPlay.sln @@ -51,25 +48,9 @@ jobs: working-directory: build-${{ github.run_id }} run: dotnet test Jellyfin.Plugin.SRFPlay.sln --no-build --configuration Release --verbosity normal - - name: Build Jellyfin Plugin - id: jprm + - name: Build Jellyfin Plugins 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|^\./||') - ARTIFACT_NAME=$(basename "${ARTIFACT}") - echo "artifact=${ARTIFACT}" >> $GITHUB_OUTPUT - echo "artifact_name=${ARTIFACT_NAME}" >> $GITHUB_OUTPUT - echo "Found artifact: ${ARTIFACT}" - - - name: Calculate checksum - id: checksum - working-directory: build-${{ github.run_id }} - run: | - CHECKSUM=$(md5sum "${{ steps.jprm.outputs.artifact }}" | awk '{print $1}') - echo "checksum=${CHECKSUM}" >> $GITHUB_OUTPUT - echo "Checksum: ${CHECKSUM}" + run: .gitea/scripts/build-plugins.sh "${{ steps.version.outputs.format }}" - name: Delete existing nightly release working-directory: build-${{ github.run_id }} @@ -129,13 +110,15 @@ 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 one plugin zip per Jellyfin generation + while IFS=$'\t' 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 # Upload build.yaml echo "Uploading build.yaml..." @@ -155,12 +138,10 @@ jobs: REPO_OWNER="${{ github.repository_owner }}" REPO_NAME="${{ github.event.repository.name }}" GITEA_URL="${{ github.server_url }}" - VERSION="${{ steps.version.outputs.version }}" - CHECKSUM="${{ steps.checksum.outputs.checksum }}" - ARTIFACT_NAME="${{ steps.jprm.outputs.artifact_name }}" TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - DOWNLOAD_URL="${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/releases/download/latest/${ARTIFACT_NAME}" SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) + # builds.tsv is read before the checkout below replaces the tree. + BUILDS=$(cat artifacts/builds.tsv) git config user.name "Gitea Actions" git config user.email "actions@gitea.tourolle.paris" @@ -178,25 +159,24 @@ jobs: # edits must be master, not whatever this build started from. git checkout -B master origin/master - NEW_VERSION=$(cat < 0) | split("\t") + | {version: .[0], changelog: "Nightly build \(.[0]) (\($sha))", targetAbi: .[1], + sourceUrl: "\($url)/\(.[2] | split("/") | last)", checksum: .[3], timestamp: $ts}) + | reverse') - # Prepend the new build and keep only the most recent 5 nightlies. - jq --argjson newver "${NEW_VERSION}" \ - '.[0].versions = ([$newver] + .[0].versions)[0:5]' \ + # Prepend the new builds and keep only the most recent 5 nightlies + # (3 entries each, one per Jellyfin generation). + jq --argjson newvers "${NEW_VERSIONS}" \ + '.[0].versions = ($newvers + .[0].versions)[0:15]' \ manifest-nightly.json > manifest.tmp && mv manifest.tmp manifest-nightly.json git add manifest-nightly.json - git commit -m "Nightly: ${VERSION} (${SHORT_SHA})" || echo "No changes to commit" + git commit -m "Nightly: ${{ steps.version.outputs.version }} (${SHORT_SHA})" || echo "No changes to commit" git push origin master - name: Cleanup diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 7306b15..75f4e42 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: @@ -61,25 +54,11 @@ jobs: working-directory: release-${{ github.run_id }} run: dotnet test Jellyfin.Plugin.SRFPlay.sln --no-build --configuration Release --verbosity normal - - name: Build Jellyfin Plugin - id: jprm + - name: Build Jellyfin Plugins 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}" - - - 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 "Checksum: ${CHECKSUM}" + # v1.2.0 -> 1.2.0.9 / 1.2.0.11 / 1.2.0.12, one per Jellyfin generation + # (see .gitea/scripts/build-plugins.sh). + run: .gitea/scripts/build-plugins.sh "${{ steps.get_version.outputs.version_number }}.%d" - name: Create Release working-directory: release-${{ github.run_id }} @@ -111,13 +90,15 @@ 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 one plugin zip per Jellyfin generation + while IFS=$'\t' 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 # Upload build.yaml echo "Uploading build.yaml..." @@ -139,29 +120,28 @@ jobs: REPO_NAME="${{ github.event.repository.name }}" GITEA_URL="${{ github.server_url }}" VERSION="${{ steps.get_version.outputs.version_number }}" - CHECKSUM="${{ steps.checksum.outputs.checksum }}" - ARTIFACT_NAME="${{ steps.jprm.outputs.artifact_name }}" TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - DOWNLOAD_URL="${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/releases/download/${{ steps.get_version.outputs.version }}/${ARTIFACT_NAME}" + # builds.tsv is read before the checkout below replaces the tree. + BUILDS=$(cat artifacts/builds.tsv) git config user.name "Gitea Actions" git config user.email "actions@gitea.tourolle.paris" git fetch origin master - git checkout master + # -B against origin/master for the same reason as the nightly: a bare + # `checkout master` edits the manifest on a stale base if master moved. + git checkout -B master origin/master - NEW_VERSION=$(cat < 0) | split("\t") + | {version: .[0], changelog: "Release \($release)", targetAbi: .[1], + sourceUrl: "\($url)/\(.[2] | split("/") | last)", checksum: .[3], timestamp: $ts}) + | reverse') - jq --argjson newver "${NEW_VERSION}" '.[0].versions = [$newver] + .[0].versions' manifest.json > manifest.tmp && mv manifest.tmp manifest.json + jq --argjson newvers "${NEW_VERSIONS}" '.[0].versions = $newvers + .[0].versions' manifest.json > manifest.tmp && mv manifest.tmp manifest.json git add manifest.json git commit -m "Update manifest.json for version ${VERSION}" git push origin master diff --git a/Jellyfin.Plugin.SRFPlay.Tests/Jellyfin.Plugin.SRFPlay.Tests.csproj b/Jellyfin.Plugin.SRFPlay.Tests/Jellyfin.Plugin.SRFPlay.Tests.csproj index 22b4348..c5cea0d 100644 --- a/Jellyfin.Plugin.SRFPlay.Tests/Jellyfin.Plugin.SRFPlay.Tests.csproj +++ b/Jellyfin.Plugin.SRFPlay.Tests/Jellyfin.Plugin.SRFPlay.Tests.csproj @@ -5,13 +5,13 @@ - Exe - net8.0 + + net8.0;net9.0;net10.0 enable enable diff --git a/Jellyfin.Plugin.SRFPlay/Configuration/configPage.html b/Jellyfin.Plugin.SRFPlay/Configuration/configPage.html index 46f2268..1d56931 100644 --- a/Jellyfin.Plugin.SRFPlay/Configuration/configPage.html +++ b/Jellyfin.Plugin.SRFPlay/Configuration/configPage.html @@ -257,7 +257,7 @@ fetch(ApiClient.serverAddress() + '/Plugins/SRFPlay/Maintenance/ResumePoints/Cleanup?clearAll=' + (clearAll ? 'true' : 'false'), { method: 'POST', - headers: { 'X-Emby-Token': ApiClient.accessToken() } + headers: { 'Authorization': 'MediaBrowser Token="' + ApiClient.accessToken() + '"' } }) .then(function(response) { if (!response.ok) { @@ -285,7 +285,7 @@ getHeaders: function() { return { - 'X-Emby-Token': ApiClient.accessToken() + 'Authorization': 'MediaBrowser Token="' + ApiClient.accessToken() + '"' }; }, diff --git a/Jellyfin.Plugin.SRFPlay/Configuration/recordingPage.html b/Jellyfin.Plugin.SRFPlay/Configuration/recordingPage.html index efd387c..671394a 100644 --- a/Jellyfin.Plugin.SRFPlay/Configuration/recordingPage.html +++ b/Jellyfin.Plugin.SRFPlay/Configuration/recordingPage.html @@ -92,7 +92,7 @@ verifySession: function(session) { var self = this; fetch(this.serverUrl + '/System/Info', { - headers: { 'X-Emby-Token': this.token } + headers: { 'Authorization': 'MediaBrowser Token="' + this.token + '"' } }).then(function(r) { if (r.ok) { document.getElementById('srfrecUserInfo').textContent = '(Server: ' + self.serverUrl + ')'; @@ -119,7 +119,7 @@ method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-Emby-Authorization': 'MediaBrowser Client="SRF Recordings", Device="Web", DeviceId="srfrec-' + Date.now() + '", Version="1.0.0"' + 'Authorization': 'MediaBrowser Client="SRF Recordings", Device="Web", DeviceId="srfrec-' + Date.now() + '", Version="1.0.0"' }, body: JSON.stringify({ Username: user, Pw: pass }) }) @@ -151,7 +151,7 @@ }, getHeaders: function() { - return { 'X-Emby-Token': this.token }; + return { 'Authorization': 'MediaBrowser Token="' + this.token + '"' }; }, formatDate: function(dateStr) { diff --git a/Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj b/Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj index c824c13..ffcd980 100644 --- a/Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj +++ b/Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj @@ -1,7 +1,17 @@ - net8.0 + + net8.0;net9.0;net10.0 Jellyfin.Plugin.SRFPlay true true @@ -10,9 +20,30 @@ ../jellyfin.ruleset - + + $(DefineConstants);JELLYFIN_10_11_OR_GREATER + + + + $(DefineConstants);JELLYFIN_12_OR_GREATER + + + + + + + + + + + + + + + + diff --git a/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ContentRefreshTask.cs b/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ContentRefreshTask.cs index b066df9..2f5bfc2 100644 --- a/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ContentRefreshTask.cs +++ b/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ContentRefreshTask.cs @@ -104,11 +104,7 @@ public class ContentRefreshTask : IScheduledTask // Run every X hours as configured return new[] { - new TaskTriggerInfo - { - Type = TaskTriggerInfo.TriggerInterval, - IntervalTicks = TimeSpan.FromHours(intervalHours).Ticks - } + TaskTriggers.Interval(TimeSpan.FromHours(intervalHours)) }; } } diff --git a/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ExpirationCheckTask.cs b/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ExpirationCheckTask.cs index 2996afb..6053d1e 100644 --- a/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ExpirationCheckTask.cs +++ b/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ExpirationCheckTask.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Jellyfin.Plugin.SRFPlay.Services.Interfaces; +using Jellyfin.Plugin.SRFPlay.Utilities; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -87,16 +88,8 @@ public class ExpirationCheckTask : IScheduledTask // Run daily at 3 AM or every X hours as configured return new[] { - new TaskTriggerInfo - { - Type = TaskTriggerInfo.TriggerDaily, - TimeOfDayTicks = TimeSpan.FromHours(3).Ticks - }, - new TaskTriggerInfo - { - Type = TaskTriggerInfo.TriggerInterval, - IntervalTicks = TimeSpan.FromHours(intervalHours).Ticks - } + TaskTriggers.Daily(TimeSpan.FromHours(3)), + TaskTriggers.Interval(TimeSpan.FromHours(intervalHours)) }; } } diff --git a/Jellyfin.Plugin.SRFPlay/ScheduledTasks/RecordingSchedulerTask.cs b/Jellyfin.Plugin.SRFPlay/ScheduledTasks/RecordingSchedulerTask.cs index c5089ce..8a7d9b3 100644 --- a/Jellyfin.Plugin.SRFPlay/ScheduledTasks/RecordingSchedulerTask.cs +++ b/Jellyfin.Plugin.SRFPlay/ScheduledTasks/RecordingSchedulerTask.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Jellyfin.Plugin.SRFPlay.Services.Interfaces; +using Jellyfin.Plugin.SRFPlay.Utilities; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -66,11 +67,7 @@ public class RecordingSchedulerTask : IScheduledTask { return new[] { - new TaskTriggerInfo - { - Type = TaskTriggerInfo.TriggerInterval, - IntervalTicks = TimeSpan.FromSeconds(30).Ticks - } + TaskTriggers.Interval(TimeSpan.FromSeconds(30)) }; } } diff --git a/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ResumeCleanupTask.cs b/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ResumeCleanupTask.cs index 7046970..317bd65 100644 --- a/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ResumeCleanupTask.cs +++ b/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ResumeCleanupTask.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Jellyfin.Plugin.SRFPlay.Services.Interfaces; +using Jellyfin.Plugin.SRFPlay.Utilities; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -77,11 +78,7 @@ public class ResumeCleanupTask : IScheduledTask // Runs after the 3 AM expiration check so items deleted there are already gone. return new[] { - new TaskTriggerInfo - { - Type = TaskTriggerInfo.TriggerDaily, - TimeOfDayTicks = TimeSpan.FromHours(4).Ticks - } + TaskTriggers.Daily(TimeSpan.FromHours(4)) }; } } diff --git a/Jellyfin.Plugin.SRFPlay/Services/Interfaces/IResumeCleanupService.cs b/Jellyfin.Plugin.SRFPlay/Services/Interfaces/IResumeCleanupService.cs index 4560e9e..ff67020 100644 --- a/Jellyfin.Plugin.SRFPlay/Services/Interfaces/IResumeCleanupService.cs +++ b/Jellyfin.Plugin.SRFPlay/Services/Interfaces/IResumeCleanupService.cs @@ -1,6 +1,10 @@ using System.Collections.Generic; using System.Threading; +#if JELLYFIN_10_11_OR_GREATER +using Jellyfin.Database.Implementations.Entities; +#else using Jellyfin.Data.Entities; +#endif using Jellyfin.Plugin.SRFPlay.Api.Models; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Plugin.SRFPlay/Services/ResumeCleanupService.cs b/Jellyfin.Plugin.SRFPlay/Services/ResumeCleanupService.cs index 104fbbb..5178e28 100644 --- a/Jellyfin.Plugin.SRFPlay/Services/ResumeCleanupService.cs +++ b/Jellyfin.Plugin.SRFPlay/Services/ResumeCleanupService.cs @@ -3,7 +3,11 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; +#if JELLYFIN_10_11_OR_GREATER +using Jellyfin.Database.Implementations.Entities; +#else using Jellyfin.Data.Entities; +#endif using Jellyfin.Data.Enums; using Jellyfin.Plugin.SRFPlay.Api.Models; using Jellyfin.Plugin.SRFPlay.Configuration; @@ -71,7 +75,11 @@ public class ResumeCleanupService : IResumeCleanupService var channelIds = GetPluginChannelIds(); +#if JELLYFIN_12_OR_GREATER + foreach (var user in _userManager.GetUsers()) +#else foreach (var user in _userManager.Users) +#endif { cancellationToken.ThrowIfCancellationRequested(); result.UsersChecked++; diff --git a/Jellyfin.Plugin.SRFPlay/Utilities/TaskTriggers.cs b/Jellyfin.Plugin.SRFPlay/Utilities/TaskTriggers.cs new file mode 100644 index 0000000..419a66f --- /dev/null +++ b/Jellyfin.Plugin.SRFPlay/Utilities/TaskTriggers.cs @@ -0,0 +1,49 @@ +using System; +using MediaBrowser.Model.Tasks; + +namespace Jellyfin.Plugin.SRFPlay.Utilities; + +/// +/// Builds scheduled task triggers across Jellyfin versions. +/// Jellyfin 10.11 replaced the string constants on with the +/// TaskTriggerInfoType enum. A task whose GetDefaultTriggers throws is silently given a +/// 24h fallback trigger, so this must be built against the matching Jellyfin version. +/// +public static class TaskTriggers +{ + /// + /// Creates a trigger that fires repeatedly at a fixed interval. + /// + /// The interval between runs. + /// The trigger. + public static TaskTriggerInfo Interval(TimeSpan interval) + { + return new TaskTriggerInfo + { +#if JELLYFIN_10_11_OR_GREATER + Type = TaskTriggerInfoType.IntervalTrigger, +#else + Type = TaskTriggerInfo.TriggerInterval, +#endif + IntervalTicks = interval.Ticks + }; + } + + /// + /// Creates a trigger that fires once a day at the given time. + /// + /// The time of day to run at. + /// The trigger. + public static TaskTriggerInfo Daily(TimeSpan timeOfDay) + { + return new TaskTriggerInfo + { +#if JELLYFIN_10_11_OR_GREATER + Type = TaskTriggerInfoType.DailyTrigger, +#else + Type = TaskTriggerInfo.TriggerDaily, +#endif + TimeOfDayTicks = timeOfDay.Ticks + }; + } +} diff --git a/README.md b/README.md index 6eb07e2..a7d3ad2 100644 --- a/README.md +++ b/README.md @@ -99,9 +99,20 @@ Examples: ### Prerequisites -- .NET 8.0 SDK +- .NET 10.0 SDK (builds all three targets below) - Jellyfin 10.9.11 or later +The plugin is built once per Jellyfin generation, because 10.11 changed APIs in ways that +compile but fail at runtime. Installing from the repository picks the right one automatically. + +| Jellyfin | Target framework | targetAbi | +|---|---|---| +| 10.9, 10.10 | net8.0 | 10.9.0.0 | +| 10.11 | net9.0 | 10.11.0.0 | +| 12 | net10.0 | 12.0.0.0 | + +`.gitea/scripts/build-plugins.sh` builds all three zips the way CI does. + ### Build Steps ```bash @@ -111,7 +122,7 @@ dotnet build ### Output -The compiled plugin will be in `bin/Debug/net8.0/` +The compiled plugins will be in `bin/Debug/net8.0/`, `bin/Debug/net9.0/` and `bin/Debug/net10.0/` ## Manual Installation diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 8af791c..2885c48 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -100,6 +100,8 @@ + +