Add repo manifest
All checks were successful
🏗️ Build Plugin / build (push) Successful in 2m49s
🧪 Test Plugin / test (push) Successful in 1m21s
🚀 Release Plugin / build-and-release (push) Successful in 2m46s

Use title cards when non provided or is livestream
This commit is contained in:
Duncan Tourolle 2025-12-20 14:28:39 +01:00
parent dbbdd7eb6d
commit 7f71d3419c
10 changed files with 243 additions and 14 deletions

View File

@ -70,6 +70,13 @@ jobs:
echo "artifact_name=${ARTIFACT_NAME}" >> $GITHUB_OUTPUT
echo "Found artifact: ${ARTIFACT}"
- name: Calculate checksum
id: checksum
run: |
CHECKSUM=$(md5sum "${{ steps.jprm.outputs.artifact }}" | awk '{print $1}')
echo "checksum=${CHECKSUM}" >> $GITHUB_OUTPUT
echo "Checksum: ${CHECKSUM}"
- name: Create Release
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -126,3 +133,38 @@ jobs:
echo "✅ Release created successfully!"
echo "View at: ${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/releases/tag/${{ steps.get_version.outputs.version }}"
- name: Update manifest.json
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
REPO_OWNER="${{ github.repository_owner }}"
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}"
git config user.name "Gitea Actions"
git config user.email "actions@gitea.tourolle.paris"
git fetch origin master
git checkout master
NEW_VERSION=$(cat <<EOF
{
"version": "${VERSION}",
"changelog": "Release ${VERSION}",
"targetAbi": "10.9.0.0",
"sourceUrl": "${DOWNLOAD_URL}",
"checksum": "${CHECKSUM}",
"timestamp": "${TIMESTAMP}"
}
EOF
)
jq --argjson newver "${NEW_VERSION}" '.[0].versions = [$newver] + .[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

View File

@ -107,10 +107,19 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
/// <inheritdoc />
public Task<DynamicImageResponse> GetChannelImage(ImageType type, CancellationToken cancellationToken)
{
// Could provide a channel logo here
var assembly = GetType().Assembly;
var resourceStream = assembly.GetManifestResourceStream("Jellyfin.Plugin.SRFPlay.Images.logo.png");
if (resourceStream == null)
{
return Task.FromResult(new DynamicImageResponse { HasImage = false });
}
return Task.FromResult(new DynamicImageResponse
{
HasImage = false
HasImage = true,
Stream = resourceStream,
Format = MediaBrowser.Model.Drawing.ImageFormat.Png
});
}
@ -491,21 +500,36 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
// Build overview
var overview = chapter.Description ?? chapter.Lead;
// Get image URL - prefer chapter image, fall back to show image if available
var originalImageUrl = chapter.ImageUrl;
if (string.IsNullOrEmpty(originalImageUrl) && mediaComposition.Show != null)
{
originalImageUrl = mediaComposition.Show.ImageUrl;
_logger.LogDebug("URN {Urn}: Using show image as fallback", urn);
}
// Determine image URL based on configuration
string? imageUrl;
var generateTitleCards = config.GenerateTitleCards;
if (string.IsNullOrEmpty(originalImageUrl))
if (generateTitleCards)
{
_logger.LogWarning("URN {Urn}: No image URL available for '{Title}'", urn, chapter.Title);
// Generate title card with content name
_logger.LogDebug("URN {Urn}: Generating title card for '{Title}'", urn, chapter.Title);
imageUrl = CreatePlaceholderImageUrl(chapter.Title ?? "SRF Play", _mediaSourceFactory.GetServerBaseUrl());
}
else
{
// Use SRF-provided images - prefer chapter image, fall back to show image
var originalImageUrl = chapter.ImageUrl;
if (string.IsNullOrEmpty(originalImageUrl) && mediaComposition.Show != null)
{
originalImageUrl = mediaComposition.Show.ImageUrl;
_logger.LogDebug("URN {Urn}: Using show image as fallback", urn);
}
// Proxy image URL to fix Content-Type headers from SRF CDN
var imageUrl = CreateProxiedImageUrl(originalImageUrl, _mediaSourceFactory.GetServerBaseUrl());
if (string.IsNullOrEmpty(originalImageUrl))
{
_logger.LogDebug("URN {Urn}: No image URL available for '{Title}', using placeholder", urn, chapter.Title);
imageUrl = CreatePlaceholderImageUrl(chapter.Title ?? "SRF Play", _mediaSourceFactory.GetServerBaseUrl());
}
else
{
imageUrl = CreateProxiedImageUrl(originalImageUrl, _mediaSourceFactory.GetServerBaseUrl());
}
}
// Use ValidFrom for premiere date if this is a scheduled livestream, otherwise use Date
var premiereDate = chapter.Type == "SCHEDULED_LIVESTREAM" ? chapter.ValidFrom?.ToUniversalTime() : chapter.Date?.ToUniversalTime();
@ -581,6 +605,18 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
return $"{serverUrl}/Plugins/SRFPlay/Proxy/Image?url={encodedUrl}";
}
/// <summary>
/// Creates a placeholder image URL with the given text.
/// </summary>
/// <param name="text">The text to display on the placeholder.</param>
/// <param name="serverUrl">The server base URL.</param>
/// <returns>The placeholder image URL.</returns>
private static string CreatePlaceholderImageUrl(string text, string serverUrl)
{
var encodedText = Convert.ToBase64String(Encoding.UTF8.GetBytes(text));
return $"{serverUrl}/Plugins/SRFPlay/Proxy/Placeholder?text={encodedText}";
}
/// <inheritdoc />
public bool IsEnabledFor(string userId)
{

View File

@ -74,6 +74,7 @@ public class PluginConfiguration : BasePluginConfiguration
EnableTrendingContent = true;
EnableCategoryFolders = true;
EnabledTopics = new System.Collections.Generic.List<string>();
GenerateTitleCards = true;
}
/// <summary>
@ -149,4 +150,10 @@ public class PluginConfiguration : BasePluginConfiguration
/// This is important for Android and other remote clients to access streams.
/// </summary>
public string PublicServerUrl { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether to generate title card images with the content title.
/// When enabled, generates custom thumbnails instead of using SRF-provided images.
/// </summary>
public bool GenerateTitleCards { get; set; }
}

View File

@ -58,6 +58,13 @@
</label>
<div class="fieldDescription">Automatically discover and add trending videos</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="GenerateTitleCards" name="GenerateTitleCards" type="checkbox" is="emby-checkbox" />
<span>Generate Title Cards</span>
</label>
<div class="fieldDescription">Generate custom thumbnails with the content title instead of using SRF-provided images</div>
</div>
<br />
<h2>Proxy Settings</h2>
<div class="checkboxContainer checkboxContainer-withDescription">
@ -117,6 +124,7 @@
document.querySelector('#CacheDurationMinutes').value = config.CacheDurationMinutes;
document.querySelector('#EnableLatestContent').checked = config.EnableLatestContent;
document.querySelector('#EnableTrendingContent').checked = config.EnableTrendingContent;
document.querySelector('#GenerateTitleCards').checked = config.GenerateTitleCards !== false;
document.querySelector('#UseProxy').checked = config.UseProxy || false;
document.querySelector('#ProxyAddress').value = config.ProxyAddress || '';
document.querySelector('#ProxyUsername').value = config.ProxyUsername || '';
@ -137,6 +145,7 @@
config.CacheDurationMinutes = parseInt(document.querySelector('#CacheDurationMinutes').value);
config.EnableLatestContent = document.querySelector('#EnableLatestContent').checked;
config.EnableTrendingContent = document.querySelector('#EnableTrendingContent').checked;
config.GenerateTitleCards = document.querySelector('#GenerateTitleCards').checked;
config.UseProxy = document.querySelector('#UseProxy').checked;
config.ProxyAddress = document.querySelector('#ProxyAddress').value;
config.ProxyUsername = document.querySelector('#ProxyUsername').value;

View File

@ -461,4 +461,38 @@ public class StreamProxyController : ControllerBase
return StatusCode(StatusCodes.Status502BadGateway);
}
}
/// <summary>
/// Generates a placeholder image with the given text centered.
/// </summary>
/// <param name="text">The text to display (base64 encoded).</param>
/// <returns>A PNG image with the text centered.</returns>
[HttpGet("Placeholder")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult GetPlaceholder([FromQuery] string text)
{
if (string.IsNullOrEmpty(text))
{
return BadRequest("Text parameter is required");
}
string decodedText;
try
{
var bytes = Convert.FromBase64String(text);
decodedText = System.Text.Encoding.UTF8.GetString(bytes);
}
catch (FormatException)
{
_logger.LogWarning("Invalid base64 text parameter: {Text}", text);
return BadRequest("Invalid text encoding");
}
_logger.LogDebug("Generating placeholder image for: {Text}", decodedText);
var imageStream = PlaceholderImageGenerator.GeneratePlaceholder(decodedText);
return File(imageStream, "image/png");
}
}

View File

@ -14,6 +14,7 @@
<PackageReference Include="Jellyfin.Controller" Version="10.9.11" />
<PackageReference Include="Jellyfin.Model" Version="10.9.11" />
<PackageReference Include="Socks5" Version="1.1.0" />
<PackageReference Include="SkiaSharp" Version="2.88.8" />
</ItemGroup>
<ItemGroup>
@ -27,4 +28,9 @@
<EmbeddedResource Include="Configuration\configPage.html" />
</ItemGroup>
<ItemGroup>
<None Remove="..\assests\main logo.png" />
<EmbeddedResource Include="..\assests\main logo.png" LogicalName="Jellyfin.Plugin.SRFPlay.Images.logo.png" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,73 @@
using System.IO;
using SkiaSharp;
namespace Jellyfin.Plugin.SRFPlay.Utilities;
/// <summary>
/// Generates placeholder images for content without thumbnails.
/// </summary>
public static class PlaceholderImageGenerator
{
private const int Width = 640;
private const int Height = 360; // 16:9 aspect ratio
/// <summary>
/// Generates a placeholder image with the given text centered.
/// </summary>
/// <param name="text">The text to display (typically channel/show name).</param>
/// <returns>A memory stream containing the PNG image.</returns>
public static MemoryStream GeneratePlaceholder(string text)
{
using var surface = SKSurface.Create(new SKImageInfo(Width, Height));
var canvas = surface.Canvas;
// Dark gradient background
using var backgroundPaint = new SKPaint();
using var shader = SKShader.CreateLinearGradient(
new SKPoint(0, 0),
new SKPoint(Width, Height),
new[] { new SKColor(45, 45, 48), new SKColor(28, 28, 30) },
null,
SKShaderTileMode.Clamp);
backgroundPaint.Shader = shader;
canvas.DrawRect(0, 0, Width, Height, backgroundPaint);
// Text
using var textPaint = new SKPaint
{
Color = SKColors.White,
IsAntialias = true,
TextAlign = SKTextAlign.Center,
Typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Bold)
};
// Calculate font size to fit text (max 48, min 24)
float fontSize = 48;
textPaint.TextSize = fontSize;
var textWidth = textPaint.MeasureText(text);
var maxWidth = Width * 0.85f;
if (textWidth > maxWidth)
{
fontSize = fontSize * maxWidth / textWidth;
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
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
var stream = new MemoryStream();
data.SaveTo(stream);
stream.Position = 0;
return stream;
}
}

View File

@ -102,7 +102,17 @@ dotnet build
The compiled plugin will be in `bin/Debug/net8.0/`
## Installation
## 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)
2. Copy the compiled DLL to your Jellyfin plugins directory

BIN
assests/main logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

12
manifest.json Normal file
View File

@ -0,0 +1,12 @@
[
{
"guid": "a4b12f86-8c3d-4e9a-b7f2-1d5e6c8a9b4f",
"name": "SRF Play",
"description": "SRF Play plugin for Jellyfin enables streaming of content from SRF (Swiss Radio and Television). Access live TV channels and on-demand content from Switzerland's German-language public broadcaster directly within Jellyfin.",
"overview": "Stream SRF content in Jellyfin",
"owner": "dtourolle",
"category": "Live TV",
"imageUrl": "https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/raw/branch/master/assests/main%20logo.png",
"versions": []
}
]