Browse Source

add global and channel watermark overrides (#260)

* add global watermark setting

* add channel watermark override

* update changelog
pull/261/head
Jason Dove 4 years ago committed by GitHub
parent
commit
a0740de972
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 1
      ErsatzTV.Application/Channels/ChannelViewModel.cs
  3. 1
      ErsatzTV.Application/Channels/Commands/CreateChannel.cs
  4. 12
      ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
  5. 1
      ErsatzTV.Application/Channels/Commands/UpdateChannel.cs
  6. 91
      ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
  7. 5
      ErsatzTV.Application/Channels/Mapper.cs
  8. 9
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs
  9. 1
      ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs
  10. 5
      ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs
  11. 1
      ErsatzTV.Core/Domain/ConfigElementKey.cs
  12. 3
      ErsatzTV.Core/Domain/Metadata/ArtworkKind.cs
  13. 37
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  14. 1
      ErsatzTV.Core/FileSystemLayout.cs
  15. 2
      ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs
  16. 1
      ErsatzTV.Core/Interfaces/Repositories/IConfigElementRepository.cs
  17. 46
      ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs
  18. 15
      ErsatzTV.Infrastructure/Data/Repositories/ConfigElementRepository.cs
  19. 2
      ErsatzTV.Infrastructure/Images/ImageCache.cs
  20. 10
      ErsatzTV/Controllers/ArtworkController.cs
  21. 71
      ErsatzTV/Pages/ChannelEditor.razor
  22. 70
      ErsatzTV/Pages/Settings.razor
  23. 3
      ErsatzTV/ViewModels/ChannelEditViewModel.cs

2
CHANGELOG.md

@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Add watermark opacity setting to allow blending with content
- Add global watermark and channel watermark artwork
- Watermark precedence is: channel watermark, global watermark, channel logo
### Changed
- Remove unused API and SDK project; may reintroduce in the future but for now they have fallen out of date

1
ErsatzTV.Application/Channels/ChannelViewModel.cs

@ -10,6 +10,7 @@ namespace ErsatzTV.Application.Channels @@ -10,6 +10,7 @@ namespace ErsatzTV.Application.Channels
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
string Watermark,
ChannelWatermarkMode WatermarkMode,
ChannelWatermarkLocation WatermarkLocation,
ChannelWatermarkSize WatermarkSize,

1
ErsatzTV.Application/Channels/Commands/CreateChannel.cs

@ -13,6 +13,7 @@ namespace ErsatzTV.Application.Channels.Commands @@ -13,6 +13,7 @@ namespace ErsatzTV.Application.Channels.Commands
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
string Watermark,
ChannelWatermarkMode WatermarkMode,
ChannelWatermarkLocation WatermarkLocation,
ChannelWatermarkSize WatermarkSize,

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

@ -58,6 +58,18 @@ namespace ErsatzTV.Application.Channels.Commands @@ -58,6 +58,18 @@ namespace ErsatzTV.Application.Channels.Commands
});
}
if (!string.IsNullOrWhiteSpace(request.Watermark))
{
artwork.Add(
new Artwork
{
Path = request.Watermark,
ArtworkKind = ArtworkKind.Watermark,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
});
}
var channel = new Channel(Guid.NewGuid())
{
Name = name,

1
ErsatzTV.Application/Channels/Commands/UpdateChannel.cs

@ -14,6 +14,7 @@ namespace ErsatzTV.Application.Channels.Commands @@ -14,6 +14,7 @@ namespace ErsatzTV.Application.Channels.Commands
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
string Watermark,
ChannelWatermarkMode WatermarkMode,
ChannelWatermarkLocation WatermarkLocation,
ChannelWatermarkSize WatermarkSize,

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

@ -7,9 +7,11 @@ using System.Threading; @@ -7,9 +7,11 @@ using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Channels.Mapper;
using static LanguageExt.Prelude;
@ -17,28 +19,30 @@ namespace ErsatzTV.Application.Channels.Commands @@ -17,28 +19,30 @@ namespace ErsatzTV.Application.Channels.Commands
{
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
{
private readonly IChannelRepository _channelRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public UpdateChannelHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public UpdateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public Task<Either<BaseError, ChannelViewModel>> Handle(
public async Task<Either<BaseError, ChannelViewModel>> Handle(
UpdateChannel request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(c => ApplyUpdateRequest(c, request))
.Bind(v => v.ToEitherAsync());
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
}
private async Task<ChannelViewModel> ApplyUpdateRequest(Channel c, UpdateChannel update)
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)
{
c.Name = update.Name;
c.Number = update.Number;
c.FFmpegProfileId = update.FFmpegProfileId;
c.PreferredLanguageCode = update.PreferredLanguageCode;
c.Artwork ??= new List<Artwork>();
if (!string.IsNullOrWhiteSpace(update.Logo))
{
c.Artwork ??= new List<Artwork>();
Option<Artwork> maybeLogo =
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
@ -61,9 +65,40 @@ namespace ErsatzTV.Application.Channels.Commands @@ -61,9 +65,40 @@ namespace ErsatzTV.Application.Channels.Commands
});
}
if (!string.IsNullOrWhiteSpace(update.Watermark))
{
Option<Artwork> maybeWatermark =
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Watermark);
maybeWatermark.Match(
artwork =>
{
artwork.Path = update.Watermark;
artwork.DateUpdated = DateTime.UtcNow;
},
() =>
{
var artwork = new Artwork
{
Path = update.Watermark,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow,
ArtworkKind = ArtworkKind.Watermark
};
c.Artwork.Add(artwork);
});
}
else
{
c.Artwork.RemoveAll(a => a.ArtworkKind == ArtworkKind.Watermark);
}
if (update.WatermarkMode == ChannelWatermarkMode.None)
{
await _channelRepository.RemoveWatermark(c);
if (c.Watermark != null)
{
dbContext.Remove(c.Watermark);
}
}
else
{
@ -97,27 +132,37 @@ namespace ErsatzTV.Application.Channels.Commands @@ -97,27 +132,37 @@ namespace ErsatzTV.Application.Channels.Commands
}
c.StreamingMode = update.StreamingMode;
await _channelRepository.Update(c);
await dbContext.SaveChangesAsync();
return ProjectToViewModel(c);
}
private async Task<Validation<BaseError, Channel>> Validate(UpdateChannel request) =>
(await ChannelMustExist(request), ValidateName(request), await ValidateNumber(request),
private async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, UpdateChannel request) =>
(await ChannelMustExist(dbContext, request), ValidateName(request),
await ValidateNumber(dbContext, request),
ValidatePreferredLanguage(request))
.Apply((channelToUpdate, _, _, _) => channelToUpdate);
private Task<Validation<BaseError, Channel>> ChannelMustExist(UpdateChannel updateChannel) =>
_channelRepository.Get(updateChannel.ChannelId)
.Map(v => v.ToValidation<BaseError>("Channel does not exist."));
private static Task<Validation<BaseError, Channel>> ChannelMustExist(
TvContext dbContext,
UpdateChannel updateChannel) =>
dbContext.Channels
.Include(c => c.Artwork)
.Include(c => c.Watermark)
.SelectOneAsync(c => c.Id, c => c.Id == updateChannel.ChannelId)
.Map(o => o.ToValidation<BaseError>("Channel does not exist."));
private Validation<BaseError, string> ValidateName(UpdateChannel updateChannel) =>
private static Validation<BaseError, string> ValidateName(UpdateChannel updateChannel) =>
updateChannel.NotEmpty(c => c.Name)
.Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name));
private async Task<Validation<BaseError, string>> ValidateNumber(UpdateChannel updateChannel)
private static async Task<Validation<BaseError, string>> ValidateNumber(
TvContext dbContext,
UpdateChannel updateChannel)
{
Option<Channel> match = await _channelRepository.GetByNumber(updateChannel.Number);
int matchId = await match.Map(c => c.Id).IfNoneAsync(updateChannel.ChannelId);
int matchId = await dbContext.Channels
.SelectOneAsync(c => c.Number, c => c.Number == updateChannel.Number)
.Match(c => c.Id, () => updateChannel.ChannelId);
if (matchId == updateChannel.ChannelId)
{
if (Regex.IsMatch(updateChannel.Number, Channel.NumberValidator))
@ -131,7 +176,7 @@ namespace ErsatzTV.Application.Channels.Commands @@ -131,7 +176,7 @@ namespace ErsatzTV.Application.Channels.Commands
return BaseError.New("Channel number must be unique");
}
private Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
private static Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
Optional(updateChannel.PreferredLanguageCode ?? string.Empty)
.Filter(
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(

5
ErsatzTV.Application/Channels/Mapper.cs

@ -15,6 +15,7 @@ namespace ErsatzTV.Application.Channels @@ -15,6 +15,7 @@ namespace ErsatzTV.Application.Channels
GetLogo(channel),
channel.PreferredLanguageCode,
channel.StreamingMode,
GetWatermark(channel),
channel.Watermark?.Mode ?? ChannelWatermarkMode.None,
channel.Watermark?.Location ?? ChannelWatermarkLocation.BottomRight,
channel.Watermark?.Size ?? ChannelWatermarkSize.Scaled,
@ -28,5 +29,9 @@ namespace ErsatzTV.Application.Channels @@ -28,5 +29,9 @@ namespace ErsatzTV.Application.Channels
private static string GetLogo(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
.Match(a => a.Path, string.Empty);
private static string GetWatermark(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Watermark))
.Match(a => a.Path, string.Empty);
}
}

9
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs

@ -104,6 +104,15 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -104,6 +104,15 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
ConfigElementKey.FFmpegPreferredLanguageCode,
request.Settings.PreferredLanguageCode);
if (!string.IsNullOrWhiteSpace(request.Settings.Watermark))
{
await _configElementRepository.Upsert(ConfigElementKey.FFmpegWatermark, request.Settings.Watermark);
}
else
{
await _configElementRepository.Delete(ConfigElementKey.FFmpegWatermark);
}
return Unit.Default;
}
}

1
ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs

@ -7,5 +7,6 @@ @@ -7,5 +7,6 @@
public int DefaultFFmpegProfileId { get; set; }
public string PreferredLanguageCode { get; set; }
public bool SaveReports { get; set; }
public string Watermark { get; set; }
}
}

5
ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs

@ -26,6 +26,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries @@ -26,6 +26,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports);
Option<string> preferredLanguageCode =
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
Option<string> watermark =
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegWatermark);
return new FFmpegSettingsViewModel
{
@ -33,7 +35,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries @@ -33,7 +35,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
FFprobePath = await ffprobePath.IfNoneAsync(string.Empty),
DefaultFFmpegProfileId = await defaultFFmpegProfileId.IfNoneAsync(0),
SaveReports = await saveReports.IfNoneAsync(false),
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng")
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng"),
Watermark = await watermark.IfNoneAsync(string.Empty)
};
}
}

1
ErsatzTV.Core/Domain/ConfigElementKey.cs

@ -12,6 +12,7 @@ @@ -12,6 +12,7 @@
public static ConfigElementKey FFmpegDefaultResolutionId => new("ffmpeg.default_resolution_id");
public static ConfigElementKey FFmpegSaveReports => new("ffmpeg.save_reports");
public static ConfigElementKey FFmpegPreferredLanguageCode => new("ffmpeg.preferred_language_code");
public static ConfigElementKey FFmpegWatermark => new("ffmpeg.watermark");
public static ConfigElementKey SearchIndexVersion => new("search_index.version");
public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count");
public static ConfigElementKey ChannelsPageSize => new("pages.channels.page_size");

3
ErsatzTV.Core/Domain/Metadata/ArtworkKind.cs

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
Poster = 0,
Thumbnail = 1,
Logo = 2,
FanArt = 3
FanArt = 3,
Watermark = 4
}
}

37
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -5,7 +5,9 @@ using System.Threading.Tasks; @@ -5,7 +5,9 @@ using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.FFmpeg
{
@ -13,13 +15,16 @@ namespace ErsatzTV.Core.FFmpeg @@ -13,13 +15,16 @@ namespace ErsatzTV.Core.FFmpeg
{
private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
private readonly IImageCache _imageCache;
private readonly IConfigElementRepository _configElementRepository;
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
public FFmpegProcessService(
IConfigElementRepository configElementRepository,
FFmpegPlaybackSettingsCalculator ffmpegPlaybackSettingsService,
IFFmpegStreamSelector ffmpegStreamSelector,
IImageCache imageCache)
{
_configElementRepository = configElementRepository;
_playbackSettingsCalculator = ffmpegPlaybackSettingsService;
_ffmpegStreamSelector = ffmpegStreamSelector;
_imageCache = imageCache;
@ -46,11 +51,33 @@ namespace ErsatzTV.Core.FFmpeg @@ -46,11 +51,33 @@ namespace ErsatzTV.Core.FFmpeg
start,
now);
Option<string> maybeWatermarkPath = channel.Artwork
.Filter(_ => channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect)
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
.HeadOrNone()
.Map(a => _imageCache.GetPathForImage(a.Path, ArtworkKind.Logo, Option<int>.None));
Option<string> maybeWatermarkPath = None;
if (channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect)
{
// check for channel watermark
maybeWatermarkPath = channel.Artwork
.Filter(a => a.ArtworkKind == ArtworkKind.Watermark)
.HeadOrNone()
.Map(a => _imageCache.GetPathForImage(a.Path, ArtworkKind.Watermark, Option<int>.None));
// check for global watermark
if (maybeWatermarkPath.IsNone)
{
maybeWatermarkPath = await _configElementRepository
.GetValue<string>(ConfigElementKey.FFmpegWatermark)
.MapT(a => _imageCache.GetPathForImage(a, ArtworkKind.Watermark, Option<int>.None));
}
// finally, check for channel logo
if (maybeWatermarkPath.IsNone)
{
maybeWatermarkPath = channel.Artwork
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
.HeadOrNone()
.Map(a => _imageCache.GetPathForImage(a.Path, ArtworkKind.Logo, Option<int>.None));
}
}
bool isAnimated = await maybeWatermarkPath.Match(
p => _imageCache.IsAnimated(p),

1
ErsatzTV.Core/FileSystemLayout.cs

@ -31,5 +31,6 @@ namespace ErsatzTV.Core @@ -31,5 +31,6 @@ namespace ErsatzTV.Core
public static readonly string ThumbnailCacheFolder = Path.Combine(ArtworkCacheFolder, "thumbnails");
public static readonly string LogoCacheFolder = Path.Combine(ArtworkCacheFolder, "logos");
public static readonly string FanArtCacheFolder = Path.Combine(ArtworkCacheFolder, "fanart");
public static readonly string WatermarkCacheFolder = Path.Combine(ArtworkCacheFolder, "watermarks");
}
}

2
ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs

@ -11,8 +11,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -11,8 +11,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<Option<Channel>> GetByNumber(string number);
Task<List<Channel>> GetAll();
Task<List<Channel>> GetAllForGuide();
Task<bool> Update(Channel channel);
Task Delete(int channelId);
Task<Unit> RemoveWatermark(Channel channel);
}
}

1
ErsatzTV.Core/Interfaces/Repositories/IConfigElementRepository.cs

@ -10,5 +10,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -10,5 +10,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<Option<ConfigElement>> Get(ConfigElementKey key);
Task<Option<T>> GetValue<T>(ConfigElementKey key);
Task Delete(ConfigElement configElement);
Task<Unit> Delete(ConfigElementKey configElementKey);
}
}

46
ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs

@ -1,8 +1,6 @@ @@ -1,8 +1,6 @@
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
@ -13,14 +11,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -13,14 +11,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{
public class ChannelRepository : IChannelRepository
{
private readonly IDbConnection _dbConnection;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public ChannelRepository(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection)
{
public ChannelRepository(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
_dbConnection = dbConnection;
}
public async Task<Option<Channel>> Get(int id)
{
@ -89,25 +83,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -89,25 +83,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ToListAsync();
}
public async Task<bool> Update(Channel channel)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
dbContext.Entry(channel).State = EntityState.Modified;
if (channel.Watermark != null)
{
dbContext.Entry(channel.Watermark).State =
channel.WatermarkId == null ? EntityState.Added : EntityState.Modified;
}
foreach (Artwork artwork in Optional(channel.Artwork).Flatten())
{
dbContext.Entry(artwork).State = artwork.Id > 0 ? EntityState.Modified : EntityState.Added;
}
bool result = await dbContext.SaveChangesAsync() > 0;
return result;
}
public async Task Delete(int channelId)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
@ -115,24 +90,5 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -115,24 +90,5 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
dbContext.Channels.Remove(channel);
await dbContext.SaveChangesAsync();
}
public async Task<Unit> RemoveWatermark(Channel channel)
{
if (channel.Watermark != null)
{
await _dbConnection.ExecuteAsync(
"UPDATE Channel SET WatermarkId = NULL WHERE Id = @ChannelId",
new { ChannelId = channel.Id });
await _dbConnection.ExecuteAsync(
"DELETE FROM ChannelWatermark WHERE Id = @WatermarkId",
new { channel.WatermarkId });
channel.Watermark = null;
channel.WatermarkId = null;
}
return Unit.Default;
}
}
}

15
ErsatzTV.Infrastructure/Data/Repositories/ConfigElementRepository.cs

@ -63,5 +63,20 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -63,5 +63,20 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
dbContext.ConfigElements.Remove(configElement);
await dbContext.SaveChangesAsync();
}
public async Task<Unit> Delete(ConfigElementKey configElementKey)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<ConfigElement> maybeExisting = await dbContext.ConfigElements
.SelectOneAsync(ce => ce.Key, ce => ce.Key == configElementKey.Key);
foreach (ConfigElement element in maybeExisting)
{
dbContext.ConfigElements.Remove(element);
}
await dbContext.SaveChangesAsync();
return Unit.Default;
}
}
}

2
ErsatzTV.Infrastructure/Images/ImageCache.cs

@ -66,6 +66,7 @@ namespace ErsatzTV.Infrastructure.Images @@ -66,6 +66,7 @@ namespace ErsatzTV.Infrastructure.Images
ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder),
ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder),
ArtworkKind.FanArt => Path.Combine(FileSystemLayout.FanArtCacheFolder, subfolder),
ArtworkKind.Watermark => Path.Combine(FileSystemLayout.WatermarkCacheFolder, subfolder),
_ => FileSystemLayout.LegacyImageCacheFolder
};
string target = Path.Combine(baseFolder, hex);
@ -124,6 +125,7 @@ namespace ErsatzTV.Infrastructure.Images @@ -124,6 +125,7 @@ namespace ErsatzTV.Infrastructure.Images
ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder),
ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder),
ArtworkKind.FanArt => Path.Combine(FileSystemLayout.FanArtCacheFolder, subfolder),
ArtworkKind.Watermark => Path.Combine(FileSystemLayout.WatermarkCacheFolder, subfolder),
_ => FileSystemLayout.LegacyImageCacheFolder
};

10
ErsatzTV/Controllers/ArtworkController.cs

@ -46,6 +46,16 @@ namespace ErsatzTV.Controllers @@ -46,6 +46,16 @@ namespace ErsatzTV.Controllers
Right: r => new PhysicalFileResult(r.FileName, r.MimeType));
}
[HttpGet("/artwork/watermarks/{fileName}")]
public async Task<IActionResult> GetWatermark(string fileName)
{
Either<BaseError, CachedImagePathViewModel> cachedImagePath =
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Watermark));
return cachedImagePath.Match<IActionResult>(
Left: _ => new NotFoundResult(),
Right: r => new PhysicalFileResult(r.FileName, r.MimeType));
}
[HttpGet("/artwork/fanart/{fileName}")]
public async Task<IActionResult> GetFanArt(string fileName)
{

71
ErsatzTV/Pages/ChannelEditor.razor

@ -68,6 +68,37 @@ @@ -68,6 +68,37 @@
<div style="padding-bottom: 16px; padding-top: 20px;">
<MudText Typo="Typo.h6">Watermark Settings</MudText>
</div>
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
<MudItem xs="6">
<InputFile id="watermarkFileInput" OnChange="UploadWatermark" hidden/>
@if (!string.IsNullOrWhiteSpace(_model.Watermark))
{
<MudElement HtmlTag="img" src="@($"artwork/watermarks/{_model.Watermark}")" Style="max-height: 50px"/>
}
</MudItem>
<MudItem xs="6">
@if (string.IsNullOrWhiteSpace(_model.Watermark))
{
<MudButton Class="ml-auto" HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.CloudUpload"
for="watermarkFileInput">
Upload Watermark
</MudButton>
}
else
{
<MudButton Class="ml-auto" HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Error"
StartIcon="@Icons.Material.Filled.DeleteForever"
OnClick="DeleteWatermark">
Delete Watermark
</MudButton>
}
</MudItem>
</MudGrid>
<MudSelect Class="mt-3" Label="Mode" @bind-Value="_model.WatermarkMode"
For="@(() => _model.WatermarkMode)"
Disabled="@(_model.StreamingMode == StreamingMode.HttpLiveStreamingDirect)">
@ -194,6 +225,7 @@ @@ -194,6 +225,7 @@
_model.StreamingMode = channelViewModel.StreamingMode;
_model.PreferredLanguageCode = channelViewModel.PreferredLanguageCode;
_model.WatermarkMode = channelViewModel.WatermarkMode;
_model.Watermark = channelViewModel.Watermark;
_model.WatermarkLocation = channelViewModel.WatermarkLocation;
_model.WatermarkSize = channelViewModel.WatermarkSize;
_model.WatermarkWidth = channelViewModel.WatermarkWidth;
@ -218,6 +250,7 @@ @@ -218,6 +250,7 @@
_model.FFmpegProfileId = ffmpegSettings.DefaultFFmpegProfileId;
_model.StreamingMode = StreamingMode.TransportStream;
_model.WatermarkMode = ChannelWatermarkMode.None;
_model.Watermark = string.Empty;
_model.WatermarkLocation = ChannelWatermarkLocation.BottomRight;
_model.WatermarkSize = ChannelWatermarkSize.Scaled;
_model.WatermarkWidth = 15;
@ -289,5 +322,43 @@ @@ -289,5 +322,43 @@
_logger.LogError("Unexpected error saving channel logo: {Error}", ex.Message);
}
}
private async Task UploadWatermark(InputFileChangeEventArgs e)
{
try
{
var buffer = new byte[e.File.Size];
await e.File.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024).ReadAsync(buffer);
Either<BaseError, string> maybeCacheFileName = await _mediator
.Send(new SaveArtworkToDisk(buffer, ArtworkKind.Watermark));
maybeCacheFileName.Match(
relativeFileName =>
{
_model.Watermark = relativeFileName;
StateHasChanged();
},
error =>
{
_snackbar.Add($"Unexpected error saving watermark: {error.Value}", Severity.Error);
_logger.LogError("Unexpected error saving watermark: {Error}", error.Value);
});
}
catch (IOException)
{
_snackbar.Add("Watermark exceeds maximum allowed file size of 10 MB", Severity.Error);
_logger.LogError("Watermark exceeds maximum allowed file size of 10 MB");
}
catch (Exception ex)
{
_snackbar.Add($"Unexpected error saving watermark: {ex.Message}", Severity.Error);
_logger.LogError("Unexpected error saving watermark: {Error}", ex.Message);
}
}
private void DeleteWatermark()
{
_model.Watermark = null;
StateHasChanged();
}
}

70
ErsatzTV/Pages/Settings.razor

@ -9,6 +9,7 @@ @@ -9,6 +9,7 @@
@using ErsatzTV.Application.Configuration.Queries
@using Unit = LanguageExt.Unit
@using ErsatzTV.Application.Configuration.Commands
@using ErsatzTV.Application.Images.Commands
@inject IMediator _mediator
@inject ISnackbar _snackbar
@inject ILogger<Settings> _logger
@ -46,6 +47,37 @@ @@ -46,6 +47,37 @@
Color="Color.Primary"
@bind-Checked="@_ffmpegSettings.SaveReports"/>
</MudElement>
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
<MudItem xs="6">
<InputFile id="fileInput" OnChange="UploadWatermark" hidden/>
@if (!string.IsNullOrWhiteSpace(_ffmpegSettings.Watermark))
{
<MudElement HtmlTag="img" src="@($"artwork/watermarks/{_ffmpegSettings.Watermark}")" Style="max-height: 50px"/>
}
</MudItem>
<MudItem xs="6">
@if (string.IsNullOrWhiteSpace(_ffmpegSettings.Watermark))
{
<MudButton Class="ml-auto" HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.CloudUpload"
for="fileInput">
Upload Watermark
</MudButton>
}
else
{
<MudButton Class="ml-auto" HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Error"
StartIcon="@Icons.Material.Filled.DeleteForever"
OnClick="DeleteWatermark">
Delete Watermark
</MudButton>
}
</MudItem>
</MudGrid>
</MudForm>
</MudCardContent>
<MudCardActions>
@ -158,5 +190,43 @@ @@ -158,5 +190,43 @@
},
Right: _ => _snackbar.Add("Successfully saved scanner settings", Severity.Success));
}
private async Task UploadWatermark(InputFileChangeEventArgs e)
{
try
{
var buffer = new byte[e.File.Size];
await e.File.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024).ReadAsync(buffer);
Either<BaseError, string> maybeCacheFileName = await _mediator
.Send(new SaveArtworkToDisk(buffer, ArtworkKind.Watermark));
maybeCacheFileName.Match(
relativeFileName =>
{
_ffmpegSettings.Watermark = relativeFileName;
StateHasChanged();
},
error =>
{
_snackbar.Add($"Unexpected error saving watermark: {error.Value}", Severity.Error);
_logger.LogError("Unexpected error saving watermark: {Error}", error.Value);
});
}
catch (IOException)
{
_snackbar.Add("Watermark exceeds maximum allowed file size of 10 MB", Severity.Error);
_logger.LogError("Watermark exceeds maximum allowed file size of 10 MB");
}
catch (Exception ex)
{
_snackbar.Add($"Unexpected error saving watermark: {ex.Message}", Severity.Error);
_logger.LogError("Unexpected error saving watermark: {Error}", ex.Message);
}
}
private void DeleteWatermark()
{
_ffmpegSettings.Watermark = null;
StateHasChanged();
}
}

3
ErsatzTV/ViewModels/ChannelEditViewModel.cs

@ -12,6 +12,7 @@ namespace ErsatzTV.ViewModels @@ -12,6 +12,7 @@ namespace ErsatzTV.ViewModels
public string PreferredLanguageCode { get; set; }
public string Logo { get; set; }
public StreamingMode StreamingMode { get; set; }
public string Watermark { get; set; }
public ChannelWatermarkMode WatermarkMode { get; set; }
public ChannelWatermarkLocation WatermarkLocation { get; set; }
public ChannelWatermarkSize WatermarkSize { get; set; }
@ -31,6 +32,7 @@ namespace ErsatzTV.ViewModels @@ -31,6 +32,7 @@ namespace ErsatzTV.ViewModels
Logo,
PreferredLanguageCode,
StreamingMode,
Watermark,
WatermarkMode,
WatermarkLocation,
WatermarkSize,
@ -49,6 +51,7 @@ namespace ErsatzTV.ViewModels @@ -49,6 +51,7 @@ namespace ErsatzTV.ViewModels
Logo,
PreferredLanguageCode,
StreamingMode,
Watermark,
WatermarkMode,
WatermarkLocation,
WatermarkSize,

Loading…
Cancel
Save