Add nightly job
Some checks failed
🏗️ Build Plugin / build (push) Successful in 29s
Nightly Build / nightly-build (push) Has been cancelled
🧪 Test Plugin / test (push) Has been cancelled

This commit is contained in:
Duncan Tourolle 2026-06-27 11:17:51 +02:00
parent 4b5d7e2a7f
commit cedef6d6aa
9 changed files with 78 additions and 140 deletions

View File

@ -1,4 +1,4 @@
name: 'Latest Release' name: 'Nightly Build'
on: on:
push: push:
@ -7,9 +7,10 @@ on:
paths-ignore: paths-ignore:
- '**/*.md' - '**/*.md'
- 'manifest.json' - 'manifest.json'
- 'manifest-nightly.json'
jobs: jobs:
latest-release: nightly-build:
runs-on: linux/amd64 runs-on: linux/amd64
container: container:
image: gitea.tourolle.paris/dtourolle/srfplay-builder:latest image: gitea.tourolle.paris/dtourolle/srfplay-builder:latest
@ -20,6 +21,24 @@ jobs:
with: with:
path: build-${{ github.run_id }} path: build-${{ github.run_id }}
- name: Compute nightly version
id: version
run: |
# Date-based nightly version: 1.0.<YYYYMMDD>.<run_number>
# 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 - name: Restore dependencies
working-directory: build-${{ github.run_id }} working-directory: build-${{ github.run_id }}
run: dotnet restore Jellyfin.Plugin.SRFPlay.sln run: dotnet restore Jellyfin.Plugin.SRFPlay.sln
@ -52,7 +71,7 @@ jobs:
echo "checksum=${CHECKSUM}" >> $GITHUB_OUTPUT echo "checksum=${CHECKSUM}" >> $GITHUB_OUTPUT
echo "Checksum: ${CHECKSUM}" echo "Checksum: ${CHECKSUM}"
- name: Delete existing latest release - name: Delete existing nightly release
working-directory: build-${{ github.run_id }} working-directory: build-${{ github.run_id }}
env: env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -71,7 +90,7 @@ jobs:
-H "Authorization: token ${GITEA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/${TAG}" | jq -r '.id') "${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 \ curl -s -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}" "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}"
@ -81,7 +100,7 @@ jobs:
-H "Authorization: token ${GITEA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/tags/${TAG}" || true "${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 }} working-directory: build-${{ github.run_id }}
env: env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -89,12 +108,14 @@ jobs:
REPO_OWNER="${{ github.repository_owner }}" REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}" REPO_NAME="${{ github.event.repository.name }}"
GITEA_URL="${{ github.server_url }}" 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 \ RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases" \ "${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) HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d') BODY=$(echo "$RESPONSE" | sed '$d')
@ -124,9 +145,9 @@ jobs:
--data-binary "@build.yaml" \ --data-binary "@build.yaml" \
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=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 }} working-directory: build-${{ github.run_id }}
env: env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -134,6 +155,7 @@ jobs:
REPO_OWNER="${{ github.repository_owner }}" REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}" REPO_NAME="${{ github.event.repository.name }}"
GITEA_URL="${{ github.server_url }}" GITEA_URL="${{ github.server_url }}"
VERSION="${{ steps.version.outputs.version }}"
CHECKSUM="${{ steps.checksum.outputs.checksum }}" CHECKSUM="${{ steps.checksum.outputs.checksum }}"
ARTIFACT_NAME="${{ steps.jprm.outputs.artifact_name }}" ARTIFACT_NAME="${{ steps.jprm.outputs.artifact_name }}"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
@ -145,13 +167,10 @@ jobs:
git fetch origin master git fetch origin master
git checkout 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 <<EOF NEW_VERSION=$(cat <<EOF
{ {
"version": "0.0.0.0", "version": "${VERSION}",
"changelog": "Latest Build", "changelog": "Nightly build ${VERSION} (${SHORT_SHA})",
"targetAbi": "10.9.0.0", "targetAbi": "10.9.0.0",
"sourceUrl": "${DOWNLOAD_URL}", "sourceUrl": "${DOWNLOAD_URL}",
"checksum": "${CHECKSUM}", "checksum": "${CHECKSUM}",
@ -160,9 +179,13 @@ jobs:
EOF EOF
) )
jq --argjson newver "${NEW_VERSION}" '.[0].versions = [$newver] + .[0].versions' manifest.json > manifest.tmp && mv manifest.tmp manifest.json # Prepend the new build and keep only the most recent 5 nightlies.
git add manifest.json jq --argjson newver "${NEW_VERSION}" \
git commit -m "Update manifest.json for latest build (${SHORT_SHA})" || echo "No changes to commit" '.[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 git push origin master
- name: Cleanup - name: Cleanup

View File

@ -20,7 +20,6 @@ namespace Jellyfin.Plugin.SRFPlay.Api;
/// </summary> /// </summary>
public class SRFApiClient : IDisposable public class SRFApiClient : IDisposable
{ {
private static readonly System.Text.CompositeFormat PlayV3UrlFormat = System.Text.CompositeFormat.Parse(ApiEndpoints.PlayV3BaseUrlTemplate);
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly HttpClient _playV3HttpClient; private readonly HttpClient _playV3HttpClient;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -58,6 +57,21 @@ public class SRFApiClient : IDisposable
}; };
} }
/// <summary>
/// Builds the Play v3 API base URL for a business unit. The host is normally
/// www.&lt;unit&gt;.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.
/// </summary>
/// <param name="businessUnit">The lowercase business unit (e.g. "srf", "swi").</param>
/// <returns>The Play v3 production base URL ending in a slash.</returns>
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/";
}
/// <summary> /// <summary>
/// Reads HTTP response content as UTF-8 string. /// Reads HTTP response content as UTF-8 string.
/// </summary> /// </summary>
@ -315,7 +329,7 @@ public class SRFApiClient : IDisposable
{ {
try try
{ {
var baseUrl = string.Format(CultureInfo.InvariantCulture, PlayV3UrlFormat, businessUnit); var baseUrl = BuildPlayV3BaseUrl(businessUnit);
var url = $"{baseUrl}{endpoint}"; var url = $"{baseUrl}{endpoint}";
_logger.LogInformation("Fetching all {Endpoint} for business unit: {BusinessUnit} from URL: {Url}", endpoint, businessUnit, url); _logger.LogInformation("Fetching all {Endpoint} for business unit: {BusinessUnit} from URL: {Url}", endpoint, businessUnit, url);
@ -352,7 +366,7 @@ public class SRFApiClient : IDisposable
{ {
try try
{ {
var baseUrl = string.Format(CultureInfo.InvariantCulture, PlayV3UrlFormat, businessUnit); var baseUrl = BuildPlayV3BaseUrl(businessUnit);
var url = $"{baseUrl}videos-by-show-id?showId={showId}"; var url = $"{baseUrl}videos-by-show-id?showId={showId}";
_logger.LogDebug("Fetching videos for show {ShowId} from business unit: {BusinessUnit}", showId, businessUnit); _logger.LogDebug("Fetching videos for show {ShowId} from business unit: {BusinessUnit}", showId, businessUnit);
@ -392,7 +406,7 @@ public class SRFApiClient : IDisposable
{ {
try try
{ {
var baseUrl = string.Format(CultureInfo.InvariantCulture, PlayV3UrlFormat, businessUnit); var baseUrl = BuildPlayV3BaseUrl(businessUnit);
var url = $"{baseUrl}livestreams?eventType={eventType.ToUpperInvariant()}"; var url = $"{baseUrl}livestreams?eventType={eventType.ToUpperInvariant()}";
_logger.LogInformation("Fetching scheduled livestreams for eventType={EventType} from business unit: {BusinessUnit}", eventType, businessUnit); _logger.LogInformation("Fetching scheduled livestreams for eventType={EventType} from business unit: {BusinessUnit}", eventType, businessUnit);

View File

@ -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<List<ChannelItemInfo>> GetFolderItemsAsync(string? folderId, CancellationToken cancellationToken) private async Task<List<ChannelItemInfo>> 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<ChannelItemInfo>();
}
// Root level - show folder list // Root level - show folder list
if (string.IsNullOrEmpty(folderId)) if (string.IsNullOrEmpty(folderId))
{ {

View File

@ -66,7 +66,6 @@ public class PluginConfiguration : BasePluginConfiguration
{ {
// Set default options // Set default options
BusinessUnit = BusinessUnit.SRF; BusinessUnit = BusinessUnit.SRF;
EnabledBusinessUnits = new System.Collections.Generic.List<BusinessUnit> { BusinessUnit.SRF };
QualityPreference = QualityPreference.Auto; QualityPreference = QualityPreference.Auto;
ContentRefreshIntervalHours = 6; ContentRefreshIntervalHours = 6;
ExpirationCheckIntervalHours = 24; ExpirationCheckIntervalHours = 24;
@ -79,20 +78,11 @@ public class PluginConfiguration : BasePluginConfiguration
} }
/// <summary> /// <summary>
/// Gets or sets the legacy single business unit. Retained for backwards compatibility and /// Gets or sets the legacy single business unit. Retained only for backwards compatibility
/// migration into <see cref="EnabledBusinessUnits"/>; new code should use the list instead. /// with older configs; every unit now has its own always-on channel.
/// </summary> /// </summary>
public BusinessUnit BusinessUnit { get; set; } public BusinessUnit BusinessUnit { get; set; }
/// <summary>
/// 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.
/// </summary>
[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<BusinessUnit> EnabledBusinessUnits { get; set; }
/// <summary> /// <summary>
/// Gets or sets the preferred video quality. /// Gets or sets the preferred video quality.
/// </summary> /// </summary>
@ -172,20 +162,4 @@ public class PluginConfiguration : BasePluginConfiguration
/// Gets or sets the output directory for sport livestream recordings. /// Gets or sets the output directory for sport livestream recordings.
/// </summary> /// </summary>
public string RecordingOutputPath { get; set; } = string.Empty; public string RecordingOutputPath { get; set; } = string.Empty;
/// <summary>
/// Resolves the effective set of enabled business units, migrating from the legacy single
/// <see cref="BusinessUnit"/> value when the list has not been populated yet.
/// </summary>
/// <returns>The business units that should have channels.</returns>
public System.Collections.Generic.IReadOnlyList<BusinessUnit> 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> { BusinessUnit };
}
} }

View File

@ -3,28 +3,8 @@
<div class="content-primary"> <div class="content-primary">
<form id="SRFPlayConfigForm"> <form id="SRFPlayConfigForm">
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<h3>Channels (Business Units)</h3> <h3>Channels</h3>
<div class="fieldDescription" style="margin-bottom: 0.5em;">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.</div> <div class="fieldDescription">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.</div>
<label class="emby-checkbox-label">
<input id="buSRF" type="checkbox" is="emby-checkbox" data-bu="SRF" />
<span>SRF — Schweizer Radio und Fernsehen (German)</span>
</label>
<label class="emby-checkbox-label">
<input id="buRTS" type="checkbox" is="emby-checkbox" data-bu="RTS" />
<span>RTS — Radio Télévision Suisse (French)</span>
</label>
<label class="emby-checkbox-label">
<input id="buRSI" type="checkbox" is="emby-checkbox" data-bu="RSI" />
<span>RSI — Radiotelevisione svizzera (Italian)</span>
</label>
<label class="emby-checkbox-label">
<input id="buRTR" type="checkbox" is="emby-checkbox" data-bu="RTR" />
<span>RTR — Radiotelevisiun Svizra Rumantscha (Romansh)</span>
</label>
<label class="emby-checkbox-label">
<input id="buSWI" type="checkbox" is="emby-checkbox" data-bu="SWI" />
<span>SWI — swissinfo.ch (International)</span>
</label>
</div> </div>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="QualityPreference">Quality Preference</label> <label class="selectLabel" for="QualityPreference">Quality Preference</label>
@ -155,12 +135,6 @@
.addEventListener('pageshow', function() { .addEventListener('pageshow', function() {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(SRFPlayConfig.pluginUniqueId).then(function (config) { 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('#QualityPreference').value = config.QualityPreference;
document.querySelector('#ContentRefreshIntervalHours').value = config.ContentRefreshIntervalHours; document.querySelector('#ContentRefreshIntervalHours').value = config.ContentRefreshIntervalHours;
document.querySelector('#ExpirationCheckIntervalHours').value = config.ExpirationCheckIntervalHours; document.querySelector('#ExpirationCheckIntervalHours').value = config.ExpirationCheckIntervalHours;
@ -186,11 +160,6 @@
.addEventListener('submit', function(e) { .addEventListener('submit', function(e) {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(SRFPlayConfig.pluginUniqueId).then(function (config) { 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.QualityPreference = document.querySelector('#QualityPreference').value;
config.ContentRefreshIntervalHours = parseInt(document.querySelector('#ContentRefreshIntervalHours').value); config.ContentRefreshIntervalHours = parseInt(document.querySelector('#ContentRefreshIntervalHours').value);
config.ExpirationCheckIntervalHours = parseInt(document.querySelector('#ExpirationCheckIntervalHours').value); config.ExpirationCheckIntervalHours = parseInt(document.querySelector('#ExpirationCheckIntervalHours').value);

View File

@ -64,8 +64,8 @@ public class ContentRefreshTask : IScheduledTask
return; return;
} }
// Refresh content for every enabled business unit (one channel per unit). // Refresh content for every business unit (one channel per unit).
var units = config.ResolveEnabledUnits(); var units = System.Enum.GetValues<Configuration.BusinessUnit>();
foreach (var unit in units) foreach (var unit in units)
{ {
var businessUnit = unit.ToLowerString(); var businessUnit = unit.ToLowerString();

View File

@ -147,13 +147,12 @@ public class RecordingService : IRecordingService, IDisposable
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<PlayV3TvProgram>> GetUpcomingScheduleAsync(CancellationToken cancellationToken) public async Task<IReadOnlyList<PlayV3TvProgram>> GetUpcomingScheduleAsync(CancellationToken cancellationToken)
{ {
var config = Plugin.Instance?.Configuration; var units = System.Enum.GetValues<Configuration.BusinessUnit>();
var units = config?.ResolveEnabledUnits() ?? new[] { Configuration.BusinessUnit.SRF };
using var apiClient = _apiClientFactory.CreateClient(); using var apiClient = _apiClientFactory.CreateClient();
// Aggregate sport livestreams across every enabled business unit so the recordings // Aggregate sport livestreams across every business unit so the recordings
// page shows events from all enabled languages. // page shows events from all languages.
var all = new List<PlayV3TvProgram>(); var all = new List<PlayV3TvProgram>();
foreach (var unit in units) foreach (var unit in units)
{ {

12
manifest-nightly.json Normal file
View File

@ -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": []
}
]

View File

@ -24,14 +24,6 @@
"checksum": "82f53848f2e2b15a35ea731ee69ee402", "checksum": "82f53848f2e2b15a35ea731ee69ee402",
"timestamp": "2026-06-27T07:16:38Z" "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", "version": "1.0.31",
"changelog": "Release 1.0.31", "changelog": "Release 1.0.31",
@ -40,14 +32,6 @@
"checksum": "4e499de15687e6e328ddca41c6192485", "checksum": "4e499de15687e6e328ddca41c6192485",
"timestamp": "2026-06-27T06:54:01Z" "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", "version": "1.0.30",
"changelog": "Release 1.0.30", "changelog": "Release 1.0.30",
@ -56,14 +40,6 @@
"checksum": "06731df9ba3d2dab53885c9c8ac95fa6", "checksum": "06731df9ba3d2dab53885c9c8ac95fa6",
"timestamp": "2026-05-03T16:44:57Z" "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", "version": "1.0.29",
"changelog": "Release 1.0.29", "changelog": "Release 1.0.29",
@ -72,14 +48,6 @@
"checksum": "fb745388e64299497262d9ad370d8823", "checksum": "fb745388e64299497262d9ad370d8823",
"timestamp": "2026-05-03T16:07:24Z" "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", "version": "1.0.27",
"changelog": "Release 1.0.27", "changelog": "Release 1.0.27",
@ -88,14 +56,6 @@
"checksum": "1e15e35452f7b82bf74d8c3560c15949", "checksum": "1e15e35452f7b82bf74d8c3560c15949",
"timestamp": "2026-03-07T16:40:17Z" "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", "version": "1.0.25",
"changelog": "Release 1.0.25", "changelog": "Release 1.0.25",