add global and channel watermark overrides (#260)
* add global watermark setting * add channel watermark override * update changelog
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
Poster = 0,
|
||||
Thumbnail = 1,
|
||||
Logo = 2,
|
||||
FanArt = 3
|
||||
FanArt = 3,
|
||||
Watermark = 4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user