Browse Source

allow external channel logo urls (#2067)

pull/2073/head
Jason Dove 1 year ago committed by GitHub
parent
commit
f71b6527c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 8
      ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
  3. 7
      ErsatzTV.Application/Channels/Commands/RefreshChannelListHandler.cs
  4. 54
      ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
  5. 16
      ErsatzTV.Application/Channels/Mapper.cs
  6. 6
      ErsatzTV.Core/Domain/Metadata/Artwork.cs
  7. 14
      ErsatzTV.Core/Iptv/ChannelPlaylist.cs
  8. 19
      ErsatzTV/Pages/ChannelEditor.razor
  9. 2
      ErsatzTV/Pages/Channels.razor
  10. 5
      ErsatzTV/Resources/Templates/_channel.sbntxt
  11. 12
      ErsatzTV/Validators/ChannelEditViewModelValidator.cs
  12. 5
      ErsatzTV/ViewModels/ChannelEditViewModel.cs

4
CHANGELOG.md

@ -47,6 +47,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -47,6 +47,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- e.g. `smart_collection:"one" NOT smart_collection:"two"`
- Cycles will be detected and logged, and searches with cycles will not work as expected
- Add all `ETV_*` environment variables to Troubleshooting > General info
- Add `External Logo URL` field to channel editor
- Using external (public) logos should fix channel logo display for clients that don't proxy artwork (such as Plex)
- Users who have customized the XMLTV channel template `channel.sbntxt` will need to update their templates again
- This is because the templates require different logic for external URLs vs ETV-hosted URLs
### Changed
- Start to make UI minimally responsive (functional on smaller screens)

8
ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs

@ -51,10 +51,16 @@ public class CreateChannelHandler( @@ -51,10 +51,16 @@ public class CreateChannelHandler(
var artwork = new List<Artwork>();
if (!string.IsNullOrWhiteSpace(request.Logo))
{
string logo = request.Logo;
if (logo.StartsWith("iptv/logos/", StringComparison.Ordinal))
{
logo = logo.Replace("iptv/logos/", string.Empty);
}
artwork.Add(
new Artwork
{
Path = request.Logo,
Path = logo,
ArtworkKind = ArtworkKind.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow

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

@ -3,6 +3,7 @@ using System.Net; @@ -3,6 +3,7 @@ using System.Net;
using System.Xml;
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Iptv;
using ErsatzTV.Infrastructure.Data;
@ -77,6 +78,9 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList> @@ -77,6 +78,9 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
await foreach (ChannelResult channel in GetChannels(dbContext).WithCancellation(cancellationToken))
{
bool hasLogo = !string.IsNullOrWhiteSpace(channel.ArtworkPath);
bool hasExternalLogo = hasLogo && Artwork.IsExternalUrl(channel.ArtworkPath);
var data = new
{
ChannelId = ChannelIdentifier.FromNumber(channel.Number),
@ -84,7 +88,8 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList> @@ -84,7 +88,8 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
ChannelNumber = channel.Number,
ChannelName = channel.Name,
ChannelCategories = GetCategories(channel.Categories),
ChannelHasArtwork = !string.IsNullOrWhiteSpace(channel.ArtworkPath),
ChannelHasExternalArtwork = hasExternalLogo,
ChannelHasArtwork = hasLogo,
ChannelArtworkPath = channel.ArtworkPath,
ChannelNameEncoded = WebUtility.UrlEncode(channel.Name)
};

54
ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs

@ -42,30 +42,46 @@ public class UpdateChannelHandler( @@ -42,30 +42,46 @@ public class UpdateChannelHandler(
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
c.MusicVideoCreditsTemplate = update.MusicVideoCreditsTemplate;
c.SongVideoMode = update.SongVideoMode;
c.Artwork ??= new List<Artwork>();
c.Artwork ??= [];
if (!string.IsNullOrWhiteSpace(update.Logo))
{
Option<Artwork> maybeLogo =
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
string logo = update.Logo;
if (logo.StartsWith("iptv/logos/", StringComparison.Ordinal))
{
logo = logo.Replace("iptv/logos/", string.Empty);
}
maybeLogo.Match(
artwork =>
{
artwork.Path = update.Logo;
artwork.DateUpdated = DateTime.UtcNow;
},
() =>
Option<Artwork> maybeLogo = c.Artwork.Where(a => a.ArtworkKind == ArtworkKind.Logo).HeadOrNone();
foreach (Artwork artwork in maybeLogo)
{
artwork.Path = logo;
artwork.DateUpdated = DateTime.UtcNow;
}
if (maybeLogo.IsNone)
{
var artwork = new Artwork
{
var artwork = new Artwork
{
Path = update.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow,
ArtworkKind = ArtworkKind.Logo
};
c.Artwork.Add(artwork);
});
Path = logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow,
ArtworkKind = ArtworkKind.Logo
};
c.Artwork.Add(artwork);
}
}
else
{
await dbContext.Entry(c)
.Collection(channel => channel.Artwork)
.LoadAsync();
foreach (Artwork artwork in c.Artwork.Where(x => x.ArtworkKind is ArtworkKind.Logo).ToList())
{
c.Artwork.Remove(artwork);
dbContext.Artwork.Remove(artwork);
}
}
c.ProgressMode = update.ProgressMode;

16
ErsatzTV.Application/Channels/Mapper.cs

@ -42,9 +42,19 @@ internal static class Mapper @@ -42,9 +42,19 @@ internal static class Mapper
internal static ResolutionAndBitrateViewModel ProjectToViewModel(Resolution resolution, int bitrate) =>
new(resolution.Height, resolution.Width, bitrate);
private static string GetLogo(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
.Match(a => a.Path, string.Empty);
private static string GetLogo(Channel channel)
{
Option<Artwork> maybeArtwork = channel.Artwork
.Where(a => a.ArtworkKind == ArtworkKind.Logo)
.HeadOrNone();
foreach (Artwork artwork in maybeArtwork)
{
return artwork.IsExternalUrl() ? artwork.Path : $"iptv/logos/{artwork.Path}";
}
return string.Empty;
}
private static string GetStreamingMode(Channel channel) =>
channel.StreamingMode switch

6
ErsatzTV.Core/Domain/Metadata/Artwork.cs

@ -11,4 +11,10 @@ public class Artwork @@ -11,4 +11,10 @@ public class Artwork
public ArtworkKind ArtworkKind { get; set; }
public DateTime DateAdded { get; set; }
public DateTime DateUpdated { get; set; }
public bool IsExternalUrl() => IsExternalUrl(Path);
public static bool IsExternalUrl(string path) =>
Uri.TryCreate(path ?? string.Empty, UriKind.Absolute, out Uri uriResult)
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
}

14
ErsatzTV.Core/Iptv/ChannelPlaylist.cs

@ -60,12 +60,16 @@ public class ChannelPlaylist @@ -60,12 +60,16 @@ public class ChannelPlaylist
sb.AppendLine("#KODIPROP:inputstream.ffmpegdirect.open_mode=ffmpeg");
}
string logo = Optional(channel.Artwork).Flatten()
Option<Artwork> maybeArtwork = Optional(channel.Artwork).Flatten()
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
.HeadOrNone()
.Match(
artwork => $"{_scheme}://{_host}{_baseUrl}/iptv/logos/{artwork.Path}.jpg{accessTokenUri}",
() => $"{_scheme}://{_host}{_baseUrl}/iptv/logos/gen?text={channel.WebEncodedName}{accessTokenUriAmp}");
.HeadOrNone();
var logo = $"{_scheme}://{_host}{_baseUrl}/iptv/logos/gen?text={channel.WebEncodedName}{accessTokenUriAmp}";
foreach (Artwork artwork in maybeArtwork)
{
logo = artwork.IsExternalUrl()
? artwork.Path
: $"{_scheme}://{_host}{_baseUrl}/iptv/logos/{artwork.Path}.jpg{accessTokenUri}";
}
string shortUniqueId = Convert.ToBase64String(channel.UniqueId.ToByteArray())
.TrimEnd('=')

19
ErsatzTV/Pages/ChannelEditor.razor

@ -99,9 +99,9 @@ @@ -99,9 +99,9 @@
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
<MudItem xs="6">
<InputFile id="fileInput" OnChange="UploadLogo" hidden/>
@if (!string.IsNullOrWhiteSpace(_model.Logo))
@if (!string.IsNullOrWhiteSpace(_model.Logo) || !string.IsNullOrWhiteSpace(_model.ExternalLogoUrl))
{
<MudElement HtmlTag="img" src="@($"iptv/logos/{_model.Logo}")" Style="max-height: 50px"/>
<MudElement HtmlTag="img" src="@(string.IsNullOrWhiteSpace(_model.ExternalLogoUrl) ? _model.Logo : _model.ExternalLogoUrl)" Style="max-height: 50px"/>
}
</MudItem>
<MudItem xs="6">
@ -114,6 +114,7 @@ @@ -114,6 +114,7 @@
</MudButton>
</MudItem>
</MudGrid>
<MudTextField Label="External Logo URL" @bind-Value="_model.ExternalLogoUrl" For="@(() => _model.ExternalLogoUrl)"/>
<MudSelect Class="mt-3" Label="Watermark" @bind-Value="_model.WatermarkId" For="@(() => _model.WatermarkId)"
Disabled="@(_model.StreamingMode == StreamingMode.HttpLiveStreamingDirect)"
Clearable="true">
@ -187,7 +188,16 @@ @@ -187,7 +188,16 @@
_model.Categories = channelViewModel.Categories;
_model.Number = channelViewModel.Number;
_model.FFmpegProfileId = channelViewModel.FFmpegProfileId;
_model.Logo = channelViewModel.Logo;
if (Artwork.IsExternalUrl(channelViewModel.Logo))
{
_model.ExternalLogoUrl = channelViewModel.Logo;
}
else
{
_model.Logo = channelViewModel.Logo;
}
_model.ProgressMode = channelViewModel.ProgressMode;
_model.StreamingMode = channelViewModel.StreamingMode;
_model.PreferredAudioLanguageCode = channelViewModel.PreferredAudioLanguageCode;
@ -265,7 +275,8 @@ @@ -265,7 +275,8 @@
maybeCacheFileName.Match(
relativeFileName =>
{
_model.Logo = relativeFileName;
_model.Logo = $"iptv/logos/{relativeFileName}";
_model.ExternalLogoUrl = null;
StateHasChanged();
},
error =>

2
ErsatzTV/Pages/Channels.razor

@ -45,7 +45,7 @@ @@ -45,7 +45,7 @@
<MudTd DataLabel="Logo">
@if (!string.IsNullOrWhiteSpace(context.Logo))
{
<MudElement HtmlTag="img" src="@($"iptv/logos/{context.Logo}")" Style="max-height: 50px"/>
<MudElement HtmlTag="img" src="@context.Logo" Style="max-height: 50px"/>
}
else
{

5
ErsatzTV/Resources/Templates/_channel.sbntxt

@ -7,6 +7,7 @@ Available values: @@ -7,6 +7,7 @@ Available values:
- channel_name
- channel_categories
- channel_has_artwork
- channel_has_external_artwork
- channel_artwork_path
- channel_name_encoded
@ -25,7 +26,9 @@ The resulting XML will be minified by ErsatzTV - so feel free to keep things nic @@ -25,7 +26,9 @@ The resulting XML will be minified by ErsatzTV - so feel free to keep things nic
{{ for category in channel_categories }}
<category lang="en">{{ category }}</category>
{{ end }}
{{ if channel_has_artwork }}
{{ if channel_has_external_artwork }}
<icon src="{{ channel_artwork_path }}" />
{{ else if channel_has_artwork }}
<icon src="{RequestBase}/iptv/logos/{{ channel_artwork_path }}.jpg{AccessTokenUri}" />
{{ else }}
<icon src="{RequestBase}/iptv/logos/gen{AccessTokenUri}&text={{ channel_name_encoded }}" />

12
ErsatzTV/Validators/ChannelEditViewModelValidator.cs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using System.Globalization;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
using ErsatzTV.ViewModels;
using FluentValidation;
@ -15,5 +14,14 @@ public class ChannelEditViewModelValidator : AbstractValidator<ChannelEditViewMo @@ -15,5 +14,14 @@ public class ChannelEditViewModelValidator : AbstractValidator<ChannelEditViewMo
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.Group).NotEmpty();
RuleFor(x => x.FFmpegProfileId).GreaterThan(0);
When(
x => !string.IsNullOrWhiteSpace(x.ExternalLogoUrl),
() =>
{
RuleFor(x => x.ExternalLogoUrl)
.Must(Artwork.IsExternalUrl)
.WithMessage("External logo url is invalid");
});
}
}

5
ErsatzTV/ViewModels/ChannelEditViewModel.cs

@ -15,6 +15,7 @@ public class ChannelEditViewModel @@ -15,6 +15,7 @@ public class ChannelEditViewModel
public string PreferredAudioLanguageCode { get; set; }
public string PreferredAudioTitle { get; set; }
public string Logo { get; set; }
public string ExternalLogoUrl { get; set; }
public ChannelProgressMode ProgressMode { get; set; }
public StreamingMode StreamingMode { get; set; }
public int? WatermarkId { get; set; }
@ -39,7 +40,7 @@ public class ChannelEditViewModel @@ -39,7 +40,7 @@ public class ChannelEditViewModel
Group,
Categories,
FFmpegProfileId,
Logo,
string.IsNullOrWhiteSpace(ExternalLogoUrl) ? Logo : ExternalLogoUrl,
PreferredAudioLanguageCode,
PreferredAudioTitle,
ProgressMode,
@ -59,7 +60,7 @@ public class ChannelEditViewModel @@ -59,7 +60,7 @@ public class ChannelEditViewModel
Group,
Categories,
FFmpegProfileId,
Logo,
string.IsNullOrWhiteSpace(ExternalLogoUrl) ? Logo : ExternalLogoUrl,
PreferredAudioLanguageCode,
PreferredAudioTitle,
ProgressMode,

Loading…
Cancel
Save