From cedef6d6aa6be8d28f6a7c389359ac36eabf7948 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 27 Jun 2026 11:17:51 +0200 Subject: [PATCH] Add nightly job --- .gitea/workflows/latest.yaml | 55 +++++++++++++------ Jellyfin.Plugin.SRFPlay/Api/SRFApiClient.cs | 22 ++++++-- .../Channels/SrgChannelBase.cs | 13 ----- .../Configuration/PluginConfiguration.cs | 30 +--------- .../Configuration/configPage.html | 35 +----------- .../ScheduledTasks/ContentRefreshTask.cs | 4 +- .../Services/RecordingService.cs | 7 +-- manifest-nightly.json | 12 ++++ manifest.json | 40 -------------- 9 files changed, 78 insertions(+), 140 deletions(-) create mode 100644 manifest-nightly.json diff --git a/.gitea/workflows/latest.yaml b/.gitea/workflows/latest.yaml index 76e5077..b5d8339 100644 --- a/.gitea/workflows/latest.yaml +++ b/.gitea/workflows/latest.yaml @@ -1,4 +1,4 @@ -name: 'Latest Release' +name: 'Nightly Build' on: push: @@ -7,9 +7,10 @@ on: paths-ignore: - '**/*.md' - 'manifest.json' + - 'manifest-nightly.json' jobs: - latest-release: + nightly-build: runs-on: linux/amd64 container: image: gitea.tourolle.paris/dtourolle/srfplay-builder:latest @@ -20,6 +21,24 @@ jobs: with: path: build-${{ github.run_id }} + - name: Compute nightly version + id: version + run: | + # 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. + DATE=$(date -u +"%Y%m%d") + VERSION="1.0.${DATE}.${{ github.run_number }}" + echo "version=${VERSION}" >> $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 @@ -52,7 +71,7 @@ jobs: echo "checksum=${CHECKSUM}" >> $GITHUB_OUTPUT echo "Checksum: ${CHECKSUM}" - - name: Delete existing latest release + - name: Delete existing nightly release working-directory: build-${{ github.run_id }} env: GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -71,7 +90,7 @@ jobs: -H "Authorization: token ${GITEA_TOKEN}" \ "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/${TAG}" | jq -r '.id') - echo "Deleting existing latest release (ID: ${RELEASE_ID})..." + echo "Deleting existing nightly release (ID: ${RELEASE_ID})..." curl -s -X DELETE \ -H "Authorization: token ${GITEA_TOKEN}" \ "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}" @@ -81,7 +100,7 @@ jobs: -H "Authorization: token ${GITEA_TOKEN}" \ "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/tags/${TAG}" || true - - name: Create latest release + - name: Create nightly release working-directory: build-${{ github.run_id }} env: GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -89,12 +108,14 @@ jobs: REPO_OWNER="${{ github.repository_owner }}" REPO_NAME="${{ github.event.repository.name }}" GITEA_URL="${{ github.server_url }}" + VERSION="${{ steps.version.outputs.version }}" + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) 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 "latest" --arg name "Latest Build" --arg body "SRFPlay Jellyfin Plugin latest build from master." '{tag_name: $tag, name: $name, body: $body, target_commitish: "master", draft: false, prerelease: true}')") + -d "$(jq -n --arg tag "latest" --arg name "Nightly Build ${VERSION}" --arg body "SRFPlay Jellyfin Plugin nightly build ${VERSION} from master (${SHORT_SHA})." '{tag_name: $tag, name: $name, body: $body, target_commitish: "master", draft: false, prerelease: true}')") HTTP_CODE=$(echo "$RESPONSE" | tail -n1) BODY=$(echo "$RESPONSE" | sed '$d') @@ -124,9 +145,9 @@ jobs: --data-binary "@build.yaml" \ "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=build.yaml" - echo "Latest release updated successfully!" + echo "Nightly release updated successfully!" - - name: Update manifest.json + - name: Update manifest-nightly.json working-directory: build-${{ github.run_id }} env: GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -134,6 +155,7 @@ 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") @@ -145,13 +167,10 @@ jobs: git fetch origin master git checkout master - # Remove existing "latest" entry if present, then prepend new one - jq --arg url "$DOWNLOAD_URL" 'if .[0].versions[0].changelog == "Latest Build" then .[0].versions = .[0].versions[1:] else . end' manifest.json > manifest.tmp && mv manifest.tmp manifest.json - NEW_VERSION=$(cat < manifest.tmp && mv manifest.tmp manifest.json - git add manifest.json - git commit -m "Update manifest.json for latest build (${SHORT_SHA})" || echo "No changes to commit" + # Prepend the new build and keep only the most recent 5 nightlies. + jq --argjson newver "${NEW_VERSION}" \ + '.[0].versions = ([$newver] + .[0].versions)[0:5]' \ + 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 push origin master - name: Cleanup diff --git a/Jellyfin.Plugin.SRFPlay/Api/SRFApiClient.cs b/Jellyfin.Plugin.SRFPlay/Api/SRFApiClient.cs index 8f6b0cc..8b3da36 100644 --- a/Jellyfin.Plugin.SRFPlay/Api/SRFApiClient.cs +++ b/Jellyfin.Plugin.SRFPlay/Api/SRFApiClient.cs @@ -20,7 +20,6 @@ namespace Jellyfin.Plugin.SRFPlay.Api; /// public class SRFApiClient : IDisposable { - private static readonly System.Text.CompositeFormat PlayV3UrlFormat = System.Text.CompositeFormat.Parse(ApiEndpoints.PlayV3BaseUrlTemplate); private readonly HttpClient _httpClient; private readonly HttpClient _playV3HttpClient; private readonly ILogger _logger; @@ -58,6 +57,21 @@ public class SRFApiClient : IDisposable }; } + /// + /// Builds the Play v3 API base URL for a business unit. The host is normally + /// www.<unit>.ch, but SWI's site lives at www.swissinfo.ch (www.swi.ch is an + /// unrelated welding institute), while the API path segment stays the unit code. + /// + /// The lowercase business unit (e.g. "srf", "swi"). + /// The Play v3 production base URL ending in a slash. + private static string BuildPlayV3BaseUrl(string businessUnit) + { + var host = string.Equals(businessUnit, "swi", StringComparison.OrdinalIgnoreCase) + ? "www.swissinfo.ch" + : $"www.{businessUnit}.ch"; + return $"https://{host}/play/v3/api/{businessUnit}/production/"; + } + /// /// Reads HTTP response content as UTF-8 string. /// @@ -315,7 +329,7 @@ public class SRFApiClient : IDisposable { try { - var baseUrl = string.Format(CultureInfo.InvariantCulture, PlayV3UrlFormat, businessUnit); + var baseUrl = BuildPlayV3BaseUrl(businessUnit); var url = $"{baseUrl}{endpoint}"; _logger.LogInformation("Fetching all {Endpoint} for business unit: {BusinessUnit} from URL: {Url}", endpoint, businessUnit, url); @@ -352,7 +366,7 @@ public class SRFApiClient : IDisposable { try { - var baseUrl = string.Format(CultureInfo.InvariantCulture, PlayV3UrlFormat, businessUnit); + var baseUrl = BuildPlayV3BaseUrl(businessUnit); var url = $"{baseUrl}videos-by-show-id?showId={showId}"; _logger.LogDebug("Fetching videos for show {ShowId} from business unit: {BusinessUnit}", showId, businessUnit); @@ -392,7 +406,7 @@ public class SRFApiClient : IDisposable { try { - var baseUrl = string.Format(CultureInfo.InvariantCulture, PlayV3UrlFormat, businessUnit); + var baseUrl = BuildPlayV3BaseUrl(businessUnit); var url = $"{baseUrl}livestreams?eventType={eventType.ToUpperInvariant()}"; _logger.LogInformation("Fetching scheduled livestreams for eventType={EventType} from business unit: {BusinessUnit}", eventType, businessUnit); diff --git a/Jellyfin.Plugin.SRFPlay/Channels/SrgChannelBase.cs b/Jellyfin.Plugin.SRFPlay/Channels/SrgChannelBase.cs index 3117649..e45cbda 100644 --- a/Jellyfin.Plugin.SRFPlay/Channels/SrgChannelBase.cs +++ b/Jellyfin.Plugin.SRFPlay/Channels/SrgChannelBase.cs @@ -175,21 +175,8 @@ public abstract class SrgChannelBase : IChannel, IHasCacheKey } } - private bool IsUnitEnabled() - { - var config = Plugin.Instance?.Configuration; - return config == null || config.ResolveEnabledUnits().Contains(Unit); - } - private async Task> GetFolderItemsAsync(string? folderId, CancellationToken cancellationToken) { - // If this unit is not enabled in configuration, show nothing (empty tile). - if (!IsUnitEnabled()) - { - _logger.LogDebug("Business unit {Unit} is not enabled - returning no items", Unit); - return new List(); - } - // Root level - show folder list if (string.IsNullOrEmpty(folderId)) { diff --git a/Jellyfin.Plugin.SRFPlay/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.SRFPlay/Configuration/PluginConfiguration.cs index 9929d06..519becb 100644 --- a/Jellyfin.Plugin.SRFPlay/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.SRFPlay/Configuration/PluginConfiguration.cs @@ -66,7 +66,6 @@ public class PluginConfiguration : BasePluginConfiguration { // Set default options BusinessUnit = BusinessUnit.SRF; - EnabledBusinessUnits = new System.Collections.Generic.List { BusinessUnit.SRF }; QualityPreference = QualityPreference.Auto; ContentRefreshIntervalHours = 6; ExpirationCheckIntervalHours = 24; @@ -79,20 +78,11 @@ public class PluginConfiguration : BasePluginConfiguration } /// - /// Gets or sets the legacy single business unit. Retained for backwards compatibility and - /// migration into ; new code should use the list instead. + /// Gets or sets the legacy single business unit. Retained only for backwards compatibility + /// with older configs; every unit now has its own always-on channel. /// public BusinessUnit BusinessUnit { get; set; } - /// - /// Gets or sets the list of business units to expose as channels. One channel tile is shown - /// per enabled unit, so polylingual households can browse e.g. SRF (German) and RTS (French) - /// at the same time. - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Required for configuration serialization")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "Configuration DTO")] - public System.Collections.Generic.List EnabledBusinessUnits { get; set; } - /// /// Gets or sets the preferred video quality. /// @@ -172,20 +162,4 @@ public class PluginConfiguration : BasePluginConfiguration /// Gets or sets the output directory for sport livestream recordings. /// public string RecordingOutputPath { get; set; } = string.Empty; - - /// - /// Resolves the effective set of enabled business units, migrating from the legacy single - /// value when the list has not been populated yet. - /// - /// The business units that should have channels. - public System.Collections.Generic.IReadOnlyList ResolveEnabledUnits() - { - if (EnabledBusinessUnits != null && EnabledBusinessUnits.Count > 0) - { - return EnabledBusinessUnits; - } - - // Legacy installs only had a single BusinessUnit; honour it so they keep working. - return new System.Collections.Generic.List { BusinessUnit }; - } } diff --git a/Jellyfin.Plugin.SRFPlay/Configuration/configPage.html b/Jellyfin.Plugin.SRFPlay/Configuration/configPage.html index 521320e..1efa49f 100644 --- a/Jellyfin.Plugin.SRFPlay/Configuration/configPage.html +++ b/Jellyfin.Plugin.SRFPlay/Configuration/configPage.html @@ -3,28 +3,8 @@
-

Channels (Business Units)

-
Enable a channel tile for each Swiss broadcaster you want. A separate tile appears per enabled unit, so polylingual households can browse e.g. SRF (German) and RTS (French) at the same time.
- - - - - +

Channels

+
Every Swiss broadcaster has its own channel tile: SRF (German), RTS (French), RSI (Italian), RTR (Romansh) and SWI (swissinfo.ch). They are always available — pin the ones you use from the Jellyfin home screen.
@@ -155,12 +135,6 @@ .addEventListener('pageshow', function() { Dashboard.showLoadingMsg(); ApiClient.getPluginConfiguration(SRFPlayConfig.pluginUniqueId).then(function (config) { - var enabled = (config.EnabledBusinessUnits && config.EnabledBusinessUnits.length) - ? config.EnabledBusinessUnits - : [config.BusinessUnit]; // migrate legacy single value - ['SRF','RTS','RSI','RTR','SWI'].forEach(function(bu) { - document.querySelector('#bu' + bu).checked = enabled.indexOf(bu) !== -1; - }); document.querySelector('#QualityPreference').value = config.QualityPreference; document.querySelector('#ContentRefreshIntervalHours').value = config.ContentRefreshIntervalHours; document.querySelector('#ExpirationCheckIntervalHours').value = config.ExpirationCheckIntervalHours; @@ -186,11 +160,6 @@ .addEventListener('submit', function(e) { Dashboard.showLoadingMsg(); ApiClient.getPluginConfiguration(SRFPlayConfig.pluginUniqueId).then(function (config) { - config.EnabledBusinessUnits = ['SRF','RTS','RSI','RTR','SWI'].filter(function(bu) { - return document.querySelector('#bu' + bu).checked; - }); - // Keep legacy field in sync with the first enabled unit for backwards compat. - config.BusinessUnit = config.EnabledBusinessUnits[0] || 'SRF'; config.QualityPreference = document.querySelector('#QualityPreference').value; config.ContentRefreshIntervalHours = parseInt(document.querySelector('#ContentRefreshIntervalHours').value); config.ExpirationCheckIntervalHours = parseInt(document.querySelector('#ExpirationCheckIntervalHours').value); diff --git a/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ContentRefreshTask.cs b/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ContentRefreshTask.cs index 4d9ef92..b066df9 100644 --- a/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ContentRefreshTask.cs +++ b/Jellyfin.Plugin.SRFPlay/ScheduledTasks/ContentRefreshTask.cs @@ -64,8 +64,8 @@ public class ContentRefreshTask : IScheduledTask return; } - // Refresh content for every enabled business unit (one channel per unit). - var units = config.ResolveEnabledUnits(); + // Refresh content for every business unit (one channel per unit). + var units = System.Enum.GetValues(); foreach (var unit in units) { var businessUnit = unit.ToLowerString(); diff --git a/Jellyfin.Plugin.SRFPlay/Services/RecordingService.cs b/Jellyfin.Plugin.SRFPlay/Services/RecordingService.cs index 2b14467..b3e3289 100644 --- a/Jellyfin.Plugin.SRFPlay/Services/RecordingService.cs +++ b/Jellyfin.Plugin.SRFPlay/Services/RecordingService.cs @@ -147,13 +147,12 @@ public class RecordingService : IRecordingService, IDisposable /// public async Task> GetUpcomingScheduleAsync(CancellationToken cancellationToken) { - var config = Plugin.Instance?.Configuration; - var units = config?.ResolveEnabledUnits() ?? new[] { Configuration.BusinessUnit.SRF }; + var units = System.Enum.GetValues(); using var apiClient = _apiClientFactory.CreateClient(); - // Aggregate sport livestreams across every enabled business unit so the recordings - // page shows events from all enabled languages. + // Aggregate sport livestreams across every business unit so the recordings + // page shows events from all languages. var all = new List(); foreach (var unit in units) { diff --git a/manifest-nightly.json b/manifest-nightly.json new file mode 100644 index 0000000..7feede0 --- /dev/null +++ b/manifest-nightly.json @@ -0,0 +1,12 @@ +[ + { + "guid": "a4b12f86-8c3d-4e9a-b7f2-1d5e6c8a9b4f", + "name": "SRF Play (Nightly)", + "description": "NIGHTLY/UNSTABLE builds of the SRF Play plugin for Jellyfin. Built automatically from the master branch on every push. Expect bugs. For stable releases use the regular SRF Play repository instead.", + "overview": "Nightly builds of SRF Play for Jellyfin", + "owner": "dtourolle", + "category": "Live TV", + "imageUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/raw/branch/master/assests/main%20logo.png", + "versions": [] + } +] diff --git a/manifest.json b/manifest.json index 15e9067..5658bb9 100644 --- a/manifest.json +++ b/manifest.json @@ -24,14 +24,6 @@ "checksum": "82f53848f2e2b15a35ea731ee69ee402", "timestamp": "2026-06-27T07:16:38Z" }, - { - "version": "0.0.0.0", - "changelog": "Latest Build", - "targetAbi": "10.9.0.0", - "sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/latest/srfplay_1.0.0.0.zip", - "checksum": "a3848128ff37d68f97493c33538a4d25", - "timestamp": "2026-06-27T07:15:26Z" - }, { "version": "1.0.31", "changelog": "Release 1.0.31", @@ -40,14 +32,6 @@ "checksum": "4e499de15687e6e328ddca41c6192485", "timestamp": "2026-06-27T06:54:01Z" }, - { - "version": "0.0.0.0", - "changelog": "Latest Build", - "targetAbi": "10.9.0.0", - "sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/latest/srfplay_1.0.0.0.zip", - "checksum": "1a8b8b018ff6bcdb8da4783b74253fc4", - "timestamp": "2026-06-27T06:52:52Z" - }, { "version": "1.0.30", "changelog": "Release 1.0.30", @@ -56,14 +40,6 @@ "checksum": "06731df9ba3d2dab53885c9c8ac95fa6", "timestamp": "2026-05-03T16:44:57Z" }, - { - "version": "0.0.0.0", - "changelog": "Latest Build", - "targetAbi": "10.9.0.0", - "sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/latest/srfplay_1.0.0.0.zip", - "checksum": "834e9f57206eee47ec9607ef6f65d17b", - "timestamp": "2026-05-03T16:39:53Z" - }, { "version": "1.0.29", "changelog": "Release 1.0.29", @@ -72,14 +48,6 @@ "checksum": "fb745388e64299497262d9ad370d8823", "timestamp": "2026-05-03T16:07:24Z" }, - { - "version": "0.0.0.0", - "changelog": "Latest Build", - "targetAbi": "10.9.0.0", - "sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/latest/srfplay_1.0.0.0.zip", - "checksum": "7471a63c69deb5b9a31343bb6c49075f", - "timestamp": "2026-05-03T16:05:37Z" - }, { "version": "1.0.27", "changelog": "Release 1.0.27", @@ -88,14 +56,6 @@ "checksum": "1e15e35452f7b82bf74d8c3560c15949", "timestamp": "2026-03-07T16:40:17Z" }, - { - "version": "0.0.0.0", - "changelog": "Latest Build", - "targetAbi": "10.9.0.0", - "sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/latest/srfplay_1.0.0.0.zip", - "checksum": "c7e868d23293adcc21d72e735094d9d6", - "timestamp": "2026-03-07T16:28:38Z" - }, { "version": "1.0.25", "changelog": "Release 1.0.25",