add global and channel watermark overrides (#260)

* add global watermark setting

* add channel watermark override

* update changelog
This commit is contained in:
Jason Dove
2021-06-13 21:45:52 -05:00
committed by GitHub
parent e69569ea46
commit a0740de972
23 changed files with 313 additions and 77 deletions
+2
View File
@@ -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
@@ -10,6 +10,7 @@ namespace ErsatzTV.Application.Channels
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
string Watermark,
ChannelWatermarkMode WatermarkMode,
ChannelWatermarkLocation WatermarkLocation,
ChannelWatermarkSize WatermarkSize,
@@ -13,6 +13,7 @@ namespace ErsatzTV.Application.Channels.Commands
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
string Watermark,
ChannelWatermarkMode WatermarkMode,
ChannelWatermarkLocation WatermarkLocation,
ChannelWatermarkSize WatermarkSize,
@@ -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,
@@ -14,6 +14,7 @@ namespace ErsatzTV.Application.Channels.Commands
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
string Watermark,
ChannelWatermarkMode WatermarkMode,
ChannelWatermarkLocation WatermarkLocation,
ChannelWatermarkSize WatermarkSize,
@@ -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
{
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
});
}
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
}
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
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
View File
@@ -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
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);
}
}
@@ -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;
}
}
@@ -7,5 +7,6 @@
public int DefaultFFmpegProfileId { get; set; }
public string PreferredLanguageCode { get; set; }
public bool SaveReports { get; set; }
public string Watermark { get; set; }
}
}
@@ -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
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
View File
@@ -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");
+2 -1
View File
@@ -5,6 +5,7 @@
Poster = 0,
Thumbnail = 1,
Logo = 2,
FanArt = 3
FanArt = 3,
Watermark = 4
}
}
+32 -5
View File
@@ -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
{
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
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
View File
@@ -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");
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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
{
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
.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
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;
}
}
}
@@ -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;
}
}
}
@@ -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
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
View File
@@ -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
View File
@@ -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 @@
_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 @@
_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 @@
_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
View File
@@ -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 @@
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 @@
},
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();
}
}
@@ -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
Logo,
PreferredLanguageCode,
StreamingMode,
Watermark,
WatermarkMode,
WatermarkLocation,
WatermarkSize,
@@ -49,6 +51,7 @@ namespace ErsatzTV.ViewModels
Logo,
PreferredLanguageCode,
StreamingMode,
Watermark,
WatermarkMode,
WatermarkLocation,
WatermarkSize,