Compare commits
No commits in common. "master" and "v1.0.12" have entirely different histories.
@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -58,18 +57,6 @@ 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>
|
/// <summary>
|
||||||
/// Creates an HttpClient with optional proxy configuration.
|
/// Creates an HttpClient with optional proxy configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -200,8 +187,7 @@ public class SRFApiClient : IDisposable
|
|||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
CreateNoWindow = true,
|
CreateNoWindow = true
|
||||||
StandardOutputEncoding = Encoding.UTF8
|
|
||||||
};
|
};
|
||||||
|
|
||||||
using var process = new System.Diagnostics.Process { StartInfo = processStartInfo };
|
using var process = new System.Diagnostics.Process { StartInfo = processStartInfo };
|
||||||
@ -261,12 +247,12 @@ public class SRFApiClient : IDisposable
|
|||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errorContent = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
_logger.LogDebug("Latest videos response length: {Length}", content.Length);
|
_logger.LogDebug("Latest videos response length: {Length}", content.Length);
|
||||||
|
|
||||||
var result = JsonSerializer.Deserialize<MediaComposition>(content, _jsonOptions);
|
var result = JsonSerializer.Deserialize<MediaComposition>(content, _jsonOptions);
|
||||||
@ -300,12 +286,12 @@ public class SRFApiClient : IDisposable
|
|||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errorContent = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
_logger.LogDebug("Trending videos response length: {Length}", content.Length);
|
_logger.LogDebug("Trending videos response length: {Length}", content.Length);
|
||||||
|
|
||||||
var result = JsonSerializer.Deserialize<MediaComposition>(content, _jsonOptions);
|
var result = JsonSerializer.Deserialize<MediaComposition>(content, _jsonOptions);
|
||||||
@ -335,7 +321,7 @@ public class SRFApiClient : IDisposable
|
|||||||
var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -363,12 +349,12 @@ public class SRFApiClient : IDisposable
|
|||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errorContent = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
var result = JsonSerializer.Deserialize<PlayV3DirectResponse<PlayV3Show>>(content, _jsonOptions);
|
var result = JsonSerializer.Deserialize<PlayV3DirectResponse<PlayV3Show>>(content, _jsonOptions);
|
||||||
|
|
||||||
_logger.LogInformation("Successfully fetched {Count} shows for business unit: {BusinessUnit}", result?.Data?.Count ?? 0, businessUnit);
|
_logger.LogInformation("Successfully fetched {Count} shows for business unit: {BusinessUnit}", result?.Data?.Count ?? 0, businessUnit);
|
||||||
@ -399,12 +385,12 @@ public class SRFApiClient : IDisposable
|
|||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errorContent = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
var result = JsonSerializer.Deserialize<PlayV3DirectResponse<PlayV3Topic>>(content, _jsonOptions);
|
var result = JsonSerializer.Deserialize<PlayV3DirectResponse<PlayV3Topic>>(content, _jsonOptions);
|
||||||
|
|
||||||
_logger.LogInformation("Successfully fetched {Count} topics for business unit: {BusinessUnit}", result?.Data?.Count ?? 0, businessUnit);
|
_logger.LogInformation("Successfully fetched {Count} topics for business unit: {BusinessUnit}", result?.Data?.Count ?? 0, businessUnit);
|
||||||
@ -436,12 +422,12 @@ public class SRFApiClient : IDisposable
|
|||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errorContent = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
var result = JsonSerializer.Deserialize<PlayV3Response<PlayV3Video>>(content, _jsonOptions);
|
var result = JsonSerializer.Deserialize<PlayV3Response<PlayV3Video>>(content, _jsonOptions);
|
||||||
|
|
||||||
_logger.LogDebug("Successfully fetched {Count} videos for show {ShowId}", result?.Data?.Data?.Count ?? 0, showId);
|
_logger.LogDebug("Successfully fetched {Count} videos for show {ShowId}", result?.Data?.Data?.Count ?? 0, showId);
|
||||||
@ -476,12 +462,12 @@ public class SRFApiClient : IDisposable
|
|||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errorContent = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// The response structure is: { "data": { "scheduledLivestreams": [...] } }
|
// The response structure is: { "data": { "scheduledLivestreams": [...] } }
|
||||||
var jsonDoc = JsonSerializer.Deserialize<JsonElement>(content, _jsonOptions);
|
var jsonDoc = JsonSerializer.Deserialize<JsonElement>(content, _jsonOptions);
|
||||||
|
|||||||
@ -41,25 +41,26 @@ public static class PlaceholderImageGenerator
|
|||||||
Typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Bold)
|
Typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Bold)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate font size and wrap text if needed
|
// Calculate font size to fit text (max 48, min 24)
|
||||||
float fontSize = 48;
|
float fontSize = 48;
|
||||||
textPaint.TextSize = fontSize;
|
textPaint.TextSize = fontSize;
|
||||||
|
var textWidth = textPaint.MeasureText(text);
|
||||||
var maxWidth = Width * 0.85f;
|
var maxWidth = Width * 0.85f;
|
||||||
var maxHeight = Height * 0.8f;
|
|
||||||
|
|
||||||
var lines = WrapText(text, textPaint, maxWidth, maxHeight, ref fontSize);
|
if (textWidth > maxWidth)
|
||||||
|
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
canvas.DrawText(line, Width / 2, startY, textPaint);
|
fontSize = fontSize * maxWidth / textWidth;
|
||||||
startY += lineHeight;
|
fontSize = fontSize < 24 ? 24 : fontSize;
|
||||||
|
textPaint.TextSize = fontSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Encode to PNG
|
||||||
using var image = surface.Snapshot();
|
using var image = surface.Snapshot();
|
||||||
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
|
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
|
||||||
@ -69,73 +70,4 @@ public static class PlaceholderImageGenerator
|
|||||||
stream.Position = 0;
|
stream.Position = 0;
|
||||||
return stream;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
20
README.md
20
README.md
@ -6,16 +6,6 @@ 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.
|
**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
|
## Features
|
||||||
|
|
||||||
- Access to SRF Play VOD content (video-on-demand, no DRM-protected content)
|
- Access to SRF Play VOD content (video-on-demand, no DRM-protected content)
|
||||||
@ -112,6 +102,16 @@ dotnet build
|
|||||||
|
|
||||||
The compiled plugin will be in `bin/Debug/net8.0/`
|
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
|
## Manual Installation
|
||||||
|
|
||||||
1. Build the plugin (see above)
|
1. Build the plugin (see above)
|
||||||
|
|||||||
@ -7,31 +7,6 @@
|
|||||||
"owner": "dtourolle",
|
"owner": "dtourolle",
|
||||||
"category": "Live TV",
|
"category": "Live TV",
|
||||||
"imageUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/raw/branch/master/assests/main%20logo.png",
|
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user