Browse Source

Generating Channel Logo when no logo is provided (#1807)

* Generating Channel Logo when none is provided

* Moved TTF in the cached Resources folder

* Using WebUtility.UrlEncode instead of Raw String Replace

* Fixed mistyping

* Moved Channel Logo Generator to etv.core

* Return 301 to static logo if there is any error during Logo generation

* minor fixes

* update changelog

---------

Co-authored-by: Jason Dove <1695733+jasongdove@users.noreply.github.com>
pull/1814/head
Sylvain 10 months ago committed by GitHub
parent
commit
23684f607a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 4
      ErsatzTV.Application/Channels/Commands/RefreshChannelListHandler.cs
  3. 2
      ErsatzTV.Core/ErsatzTV.Core.csproj
  4. 76
      ErsatzTV.Core/Images/ChannelLogoGenerator.cs
  5. 12
      ErsatzTV.Core/Interfaces/Images/IChannelLogoGenerator.cs
  6. 3
      ErsatzTV.Core/Iptv/ChannelPlaylist.cs
  7. 19
      ErsatzTV/Controllers/ArtworkController.cs
  8. 1
      ErsatzTV/ErsatzTV.csproj
  9. 6
      ErsatzTV/Pages/Channels.razor
  10. BIN
      ErsatzTV/Resources/Fonts/Sen.ttf
  11. 3
      ErsatzTV/Resources/Templates/_channel.sbntxt
  12. 3
      ErsatzTV/Services/RunOnce/ResourceExtractorService.cs
  13. 2
      ErsatzTV/Startup.cs

2
CHANGELOG.md

@ -30,6 +30,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -30,6 +30,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `duration`: play the referenced content for the specified duration
- `pad to next`: add items from the referenced content until the wall clock is a multiple of the specified minutes value
- `repeat`: continue building the playout from the first instruction in the YAML file
- Add channel logo generation by @raknam
- Channels without custom uploaded logos will automatically generate a logo that includes the channel name
### Fixed
- Add basic cache busting to XMLTV image URLs

4
ErsatzTV.Application/Channels/Commands/RefreshChannelListHandler.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Data.Common;
using System.Net;
using System.Xml;
using Dapper;
using ErsatzTV.Core;
@ -81,7 +82,8 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList> @@ -81,7 +82,8 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
ChannelName = channel.Name,
ChannelCategories = GetCategories(channel.Categories),
ChannelHasArtwork = !string.IsNullOrWhiteSpace(channel.ArtworkPath),
ChannelArtworkPath = channel.ArtworkPath
ChannelArtworkPath = channel.ArtworkPath,
ChannelNameEncoded = WebUtility.UrlEncode(channel.Name)
};
var scriptObject = new ScriptObject();

2
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -27,6 +27,8 @@ @@ -27,6 +27,8 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="SkiaSharp" Version="2.88.8" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.8" />
<PackageReference Include="TimeSpanParserUtil" Version="1.2.0" />
<PackageReference Include="YamlDotNet" Version="16.0.0" />
</ItemGroup>

76
ErsatzTV.Core/Images/ChannelLogoGenerator.cs

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
using ErsatzTV.Core.Interfaces.Images;
using Microsoft.Extensions.Logging;
using SkiaSharp;
namespace ErsatzTV.Core.Images;
public class ChannelLogoGenerator : IChannelLogoGenerator
{
private readonly ILogger _logger;
public ChannelLogoGenerator(
ILogger<ChannelLogoGenerator> logger)
{
_logger = logger;
}
public Either<BaseError, byte[]> GenerateChannelLogo(
string text,
int logoHeight,
int logoWidth,
CancellationToken cancellationToken)
{
try
{
using var surface = SKSurface.Create(new SKImageInfo(logoWidth, logoHeight));
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.Black);
//etv logo
string overlayImagePath = Path.Combine("wwwroot", "images", "ersatztv-500.png");
using SKBitmap overlayImage = SKBitmap.Decode(overlayImagePath);
canvas.DrawBitmap(overlayImage, new SKRect(155, 60, 205, 110));
//Custom Font
string fontPath = Path.Combine(FileSystemLayout.ResourcesCacheFolder, "Sen.ttf");
using SKTypeface fontTypeface = SKTypeface.FromFile(fontPath);
int fontSize = 30;
SKPaint paint = new SKPaint
{
Typeface = fontTypeface,
TextSize = fontSize,
IsAntialias = true,
Color = SKColors.White,
Style = SKPaintStyle.Fill,
TextAlign = SKTextAlign.Center
};
SKRect textBounds = new SKRect();
paint.MeasureText(text, ref textBounds);
// Ajuster la taille de la police si nécessaire
while (textBounds.Width > logoWidth - 10 && fontSize > 16)
{
fontSize -= 2;
paint.TextSize = fontSize;
paint.MeasureText(text, ref textBounds);
}
// Dessiner le texte
float x = logoWidth / 2f;
float y = logoHeight / 2f - textBounds.MidY;
canvas.DrawText(text, x, y, paint);
using SKImage image = surface.Snapshot();
using MemoryStream ms = new MemoryStream();
image.Encode(SKEncodedImageFormat.Png, 100).SaveTo(ms);
ms.Seek(0, SeekOrigin.Begin);
return ms.ToArray();
}
catch (Exception ex)
{
_logger.LogError("Can't generate Channel Logo ([{ErrorType}] {ErrorMessage})", ex.GetType(), ex.Message);
return BaseError.New("Can't generate Channel Logo " + ex.Message);
}
}
}

12
ErsatzTV.Core/Interfaces/Images/IChannelLogoGenerator.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Images;
public interface IChannelLogoGenerator
{
Either<BaseError, byte[]> GenerateChannelLogo(
string text,
int logoHeight,
int logoWidth,
CancellationToken cancellationToken);
}

3
ErsatzTV.Core/Iptv/ChannelPlaylist.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Globalization;
using System.Net;
using System.Text;
using ErsatzTV.Core.Domain;
@ -64,7 +65,7 @@ public class ChannelPlaylist @@ -64,7 +65,7 @@ public class ChannelPlaylist
.HeadOrNone()
.Match(
artwork => $"{_scheme}://{_host}{_baseUrl}/iptv/logos/{artwork.Path}.jpg{accessTokenUri}",
() => $"{_scheme}://{_host}{_baseUrl}/iptv/images/ersatztv-500.png{accessTokenUri}");
() => $"{_scheme}://{_host}{_baseUrl}/iptv/logos/gen?text={WebUtility.UrlEncode(channel.Name)}{accessTokenUriAmp}");
string shortUniqueId = Convert.ToBase64String(channel.UniqueId.ToByteArray())
.TrimEnd('=')

19
ErsatzTV/Controllers/ArtworkController.cs

@ -6,6 +6,7 @@ using ErsatzTV.Application.Plex; @@ -6,6 +6,7 @@ using ErsatzTV.Application.Plex;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Jellyfin;
using Flurl;
using MediatR;
@ -20,11 +21,16 @@ public class ArtworkController : ControllerBase @@ -20,11 +21,16 @@ public class ArtworkController : ControllerBase
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IMediator _mediator;
private readonly IChannelLogoGenerator _channelLogoGenerator;
public ArtworkController(IMediator mediator, IHttpClientFactory httpClientFactory)
public ArtworkController(
IMediator mediator,
IHttpClientFactory httpClientFactory,
IChannelLogoGenerator channelLogoGenerator)
{
_mediator = mediator;
_httpClientFactory = httpClientFactory;
_channelLogoGenerator = channelLogoGenerator;
}
[HttpHead("/artwork/{id}")]
@ -253,4 +259,15 @@ public class ArtworkController : ControllerBase @@ -253,4 +259,15 @@ public class ArtworkController : ControllerBase
});
#endif
}
[HttpGet("/iptv/logos/gen")]
public IActionResult GenerateChannelLogo(
string text,
CancellationToken cancellationToken)
{
return _channelLogoGenerator.GenerateChannelLogo(text, 100, 200, cancellationToken).Match<IActionResult>(
Left: _ => new RedirectResult("/iptv/images/ersatztv-500.png"),
Right: img => File(img, "image/png")
);
}
}

1
ErsatzTV/ErsatzTV.csproj

@ -59,6 +59,7 @@ @@ -59,6 +59,7 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\Fonts\Sen.ttf" />
<EmbeddedResource Include="Resources\background.png" />
<EmbeddedResource Include="Resources\Fonts\OPTIKabel-Heavy.otf" />
<EmbeddedResource Include="Resources\Fonts\Roboto-Regular.ttf" />

6
ErsatzTV/Pages/Channels.razor

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
@page "/channels"
@page "/channels"
@using System.Globalization
@using ErsatzTV.Application.Channels
@using ErsatzTV.Application.Configuration
@ -47,6 +47,10 @@ @@ -47,6 +47,10 @@
{
<MudElement HtmlTag="img" src="@($"iptv/logos/{context.Logo}")" Style="max-height: 50px"/>
}
else
{
<MudElement HtmlTag="img" src="@($"iptv/logos/gen?text={context.Name}")" Style="max-height: 50px" />
}
</MudTd>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd DataLabel="Language">@context.PreferredAudioLanguageCode</MudTd>

BIN
ErsatzTV/Resources/Fonts/Sen.ttf

Binary file not shown.

3
ErsatzTV/Resources/Templates/_channel.sbntxt

@ -6,6 +6,7 @@ Available values: @@ -6,6 +6,7 @@ Available values:
- channel_categories
- channel_has_artwork
- channel_artwork_path
- channel_name_encoded
{RequestBase} and {AccessTokenUri} are replaced dynamically when XMLTV is requested,
and must remain as-is in this template to work properly with ETV URLs.
@ -25,6 +26,6 @@ The resulting XML will be minified by ErsatzTV - so feel free to keep things nic @@ -25,6 +26,6 @@ The resulting XML will be minified by ErsatzTV - so feel free to keep things nic
{{ if channel_has_artwork }}
<icon src="{RequestBase}/iptv/logos/{{ channel_artwork_path }}.jpg{AccessTokenUri}" />
{{ else }}
<icon src="{RequestBase}/iptv/images/ersatztv-500.png{AccessTokenUri}" />
<icon src="{RequestBase}/iptv/logos/gen{AccessTokenUri}&text={{ channel_name_encoded }}" />
{{ end }}
</channel>

3
ErsatzTV/Services/RunOnce/ResourceExtractorService.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
using System.Reflection;
using System.Reflection;
using ErsatzTV.Core;
namespace ErsatzTV.Services.RunOnce;
@ -22,6 +22,7 @@ public class ResourceExtractorService : BackgroundService @@ -22,6 +22,7 @@ public class ResourceExtractorService : BackgroundService
await ExtractResource(assembly, "song_background_3.png", stoppingToken);
await ExtractResource(assembly, "ErsatzTV.png", stoppingToken);
await ExtractFontResource(assembly, "Sen.ttf", stoppingToken);
await ExtractFontResource(assembly, "Roboto-Regular.ttf", stoppingToken);
await ExtractFontResource(assembly, "OPTIKabel-Heavy.otf", stoppingToken);

2
ErsatzTV/Startup.cs

@ -16,6 +16,7 @@ using ErsatzTV.Core.Errors; @@ -16,6 +16,7 @@ using ErsatzTV.Core.Errors;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Health;
using ErsatzTV.Core.Health.Checks;
using ErsatzTV.Core.Images;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.GitHub;
@ -682,6 +683,7 @@ public class Startup @@ -682,6 +683,7 @@ public class Startup
services.AddScoped<IHardwareCapabilitiesFactory, HardwareCapabilitiesFactory>();
services.AddScoped<IMultiEpisodeShuffleCollectionEnumeratorFactory,
MultiEpisodeShuffleCollectionEnumeratorFactory>();
services.AddScoped<IChannelLogoGenerator, ChannelLogoGenerator>();
services.AddScoped<IFFmpegProcessService, FFmpegLibraryProcessService>();
services.AddScoped<IPipelineBuilderFactory, PipelineBuilderFactory>();

Loading…
Cancel
Save