Compare commits

...

6 Commits

Author SHA1 Message Date
Gitea Actions
d313b68975 Update manifest.json for version 1.0.14 2025-12-30 12:38:41 +00:00
60434abd01 Use utf-8 decode everywhere
All checks were successful
🏗️ Build Plugin / build (push) Successful in 3m4s
🧪 Test Plugin / test (push) Successful in 1m24s
🚀 Release Plugin / build-and-release (push) Successful in 2m52s
2025-12-30 13:31:13 +01:00
Gitea Actions
5ace3f4296 Update manifest.json for version 1.0.13 2025-12-21 13:02:38 +00:00
757aab1943 Break-line for placeholder titles when too long
All checks were successful
🏗️ Build Plugin / build (push) Successful in 2m57s
🧪 Test Plugin / test (push) Successful in 1m26s
🚀 Release Plugin / build-and-release (push) Successful in 2m56s
2025-12-21 13:55:11 +01:00
Gitea Actions
23d8da9ae7 Update manifest.json for version 1.0.12 2025-12-20 13:35:38 +00:00
2631c93444 moved repo install guide to top 2025-12-20 14:32:13 +01:00
4 changed files with 145 additions and 38 deletions

View File

@ -2,6 +2,7 @@ using System;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
@ -57,6 +58,18 @@ public class SRFApiClient : IDisposable
};
}
/// <summary>
/// Reads HTTP response content as UTF-8 string.
/// </summary>
/// <param name="content">The HTTP content to read.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The content as a UTF-8 decoded string.</returns>
private async Task<string> ReadAsUtf8StringAsync(HttpContent content, CancellationToken cancellationToken = default)
{
var bytes = await content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
return Encoding.UTF8.GetString(bytes);
}
/// <summary>
/// Creates an HttpClient with optional proxy configuration.
/// </summary>
@ -187,7 +200,8 @@ public class SRFApiClient : IDisposable
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8
};
using var process = new System.Diagnostics.Process { StartInfo = processStartInfo };
@ -247,12 +261,12 @@ public class SRFApiClient : IDisposable
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var errorContent = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
return null;
}
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Latest videos response length: {Length}", content.Length);
var result = JsonSerializer.Deserialize<MediaComposition>(content, _jsonOptions);
@ -286,12 +300,12 @@ public class SRFApiClient : IDisposable
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var errorContent = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
return null;
}
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Trending videos response length: {Length}", content.Length);
var result = JsonSerializer.Deserialize<MediaComposition>(content, _jsonOptions);
@ -321,7 +335,7 @@ public class SRFApiClient : IDisposable
var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
return content;
}
catch (Exception ex)
@ -349,12 +363,12 @@ public class SRFApiClient : IDisposable
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var errorContent = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
return null;
}
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
var result = JsonSerializer.Deserialize<PlayV3DirectResponse<PlayV3Show>>(content, _jsonOptions);
_logger.LogInformation("Successfully fetched {Count} shows for business unit: {BusinessUnit}", result?.Data?.Count ?? 0, businessUnit);
@ -385,12 +399,12 @@ public class SRFApiClient : IDisposable
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var errorContent = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
return null;
}
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
var result = JsonSerializer.Deserialize<PlayV3DirectResponse<PlayV3Topic>>(content, _jsonOptions);
_logger.LogInformation("Successfully fetched {Count} topics for business unit: {BusinessUnit}", result?.Data?.Count ?? 0, businessUnit);
@ -422,12 +436,12 @@ public class SRFApiClient : IDisposable
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var errorContent = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
return null;
}
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
var result = JsonSerializer.Deserialize<PlayV3Response<PlayV3Video>>(content, _jsonOptions);
_logger.LogDebug("Successfully fetched {Count} videos for show {ShowId}", result?.Data?.Data?.Count ?? 0, showId);
@ -462,12 +476,12 @@ public class SRFApiClient : IDisposable
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var errorContent = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
return null;
}
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
// The response structure is: { "data": { "scheduledLivestreams": [...] } }
var jsonDoc = JsonSerializer.Deserialize<JsonElement>(content, _jsonOptions);

View File

@ -41,26 +41,25 @@ public static class PlaceholderImageGenerator
Typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Bold)
};
// Calculate font size to fit text (max 48, min 24)
// Calculate font size and wrap text if needed
float fontSize = 48;
textPaint.TextSize = fontSize;
var textWidth = textPaint.MeasureText(text);
var maxWidth = Width * 0.85f;
var maxHeight = Height * 0.8f;
if (textWidth > maxWidth)
var lines = WrapText(text, textPaint, maxWidth, maxHeight, ref fontSize);
// Draw each line centered
var lineHeight = fontSize * 1.2f;
var totalHeight = lines.Count * lineHeight;
var startY = ((Height - totalHeight) / 2) + fontSize;
foreach (var line in lines)
{
fontSize = fontSize * maxWidth / textWidth;
fontSize = fontSize < 24 ? 24 : fontSize;
textPaint.TextSize = fontSize;
canvas.DrawText(line, Width / 2, startY, textPaint);
startY += lineHeight;
}
// Center text vertically
SKRect textBounds = default;
textPaint.MeasureText(text, ref textBounds);
var yPos = (Height / 2) - textBounds.MidY;
canvas.DrawText(text, Width / 2, yPos, textPaint);
// Encode to PNG
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
@ -70,4 +69,73 @@ public static class PlaceholderImageGenerator
stream.Position = 0;
return stream;
}
/// <summary>
/// Wraps text to fit within the specified width and height constraints.
/// </summary>
/// <param name="text">The text to wrap.</param>
/// <param name="paint">The paint to use for measuring.</param>
/// <param name="maxWidth">Maximum width for each line.</param>
/// <param name="maxHeight">Maximum total height.</param>
/// <param name="fontSize">Font size (will be adjusted if needed).</param>
/// <returns>List of text lines.</returns>
private static System.Collections.Generic.List<string> WrapText(
string text,
SKPaint paint,
float maxWidth,
float maxHeight,
ref float fontSize)
{
const float minFontSize = 20;
var words = text.Split(' ');
var lines = new System.Collections.Generic.List<string>();
while (fontSize >= minFontSize)
{
paint.TextSize = fontSize;
lines.Clear();
var currentLine = string.Empty;
foreach (var word in words)
{
var testLine = string.IsNullOrEmpty(currentLine) ? word : $"{currentLine} {word}";
var testWidth = paint.MeasureText(testLine);
if (testWidth > maxWidth && !string.IsNullOrEmpty(currentLine))
{
lines.Add(currentLine);
currentLine = word;
}
else
{
currentLine = testLine;
}
}
if (!string.IsNullOrEmpty(currentLine))
{
lines.Add(currentLine);
}
// Check if total height fits
var lineHeight = fontSize * 1.2f;
var totalHeight = lines.Count * lineHeight;
if (totalHeight <= maxHeight)
{
break;
}
// Reduce font size and try again
fontSize -= 2;
}
// If still doesn't fit, just return what we have
if (lines.Count == 0)
{
lines.Add(text);
}
return lines;
}
}

View File

@ -6,6 +6,16 @@ A Jellyfin plugin for accessing SRF Play (Swiss Radio and Television) video-on-d
**Beta/Alpha** - This plugin has been tested on two Jellyfin instances and is working. Some clients may experience issues with hardware decoding, which appears to be client-specific behavior.
## Quick Install
Add this repository URL in Jellyfin (Dashboard → Plugins → Repositories):
```
https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/raw/branch/master/manifest.json
```
Then install "SRF Play" from the plugin catalog.
## Features
- Access to SRF Play VOD content (video-on-demand, no DRM-protected content)
@ -102,16 +112,6 @@ dotnet build
The compiled plugin will be in `bin/Debug/net8.0/`
## Quick Install
Add this repository URL in Jellyfin (Dashboard → Plugins → Repositories):
```
https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/raw/branch/master/manifest.json
```
Then install "SRF Play" from the plugin catalog.
## Manual Installation
1. Build the plugin (see above)

View File

@ -7,6 +7,31 @@
"owner": "dtourolle",
"category": "Live TV",
"imageUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/raw/branch/master/assests/main%20logo.png",
"versions": []
"versions": [
{
"version": "1.0.14",
"changelog": "Release 1.0.14",
"targetAbi": "10.9.0.0",
"sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/v1.0.14/srfplay_1.0.14.0.zip",
"checksum": "49e6afe16f7abf95c099ecc1d016e725",
"timestamp": "2025-12-30T12:38:41Z"
},
{
"version": "1.0.13",
"changelog": "Release 1.0.13",
"targetAbi": "10.9.0.0",
"sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/v1.0.13/srfplay_1.0.13.0.zip",
"checksum": "ac7c1e33c926c21f8e4319da91ef079c",
"timestamp": "2025-12-21T13:02:37Z"
},
{
"version": "1.0.12",
"changelog": "Release 1.0.12",
"targetAbi": "10.9.0.0",
"sourceUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/releases/download/v1.0.12/srfplay_1.0.12.0.zip",
"checksum": "47baa02ade413253db94fe3ba0763f69",
"timestamp": "2025-12-20T13:35:38Z"
}
]
}
]