Browse Source

channel logo and watermark fixes (#2100)

* channel logo and watermark fixes

* update changelog
pull/2101/head
Jason Dove 11 months ago committed by GitHub
parent
commit
f6249d9fa4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 17
      ErsatzTV.Application/Artworks/ArtworkContentTypeModel.cs
  3. 3
      ErsatzTV.Application/Channels/ChannelViewModel.cs
  4. 5
      ErsatzTV.Application/Channels/Commands/CreateChannel.cs
  5. 5
      ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
  6. 5
      ErsatzTV.Application/Channels/Commands/UpdateChannel.cs
  7. 13
      ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
  8. 11
      ErsatzTV.Application/Channels/Mapper.cs
  9. 2
      ErsatzTV.Application/Images/Commands/SaveArtworkToDisk.cs
  10. 2
      ErsatzTV.Application/Images/Queries/GetCachedImagePath.cs
  11. 10
      ErsatzTV.Application/Images/Queries/GetCachedImagePathHandler.cs
  12. 5
      ErsatzTV.Application/Watermarks/Commands/CreateWatermark.cs
  13. 40
      ErsatzTV.Application/Watermarks/Commands/CreateWatermarkHandler.cs
  14. 5
      ErsatzTV.Application/Watermarks/Commands/UpdateWatermark.cs
  15. 10
      ErsatzTV.Application/Watermarks/Commands/UpdateWatermarkHandler.cs
  16. 5
      ErsatzTV.Application/Watermarks/Mapper.cs
  17. 2
      ErsatzTV.Application/Watermarks/Queries/GetWatermarkByIdHandler.cs
  18. 5
      ErsatzTV.Application/Watermarks/WatermarkViewModel.cs
  19. 1
      ErsatzTV.Core/Domain/ChannelWatermark.cs
  20. 1
      ErsatzTV.Core/Domain/Metadata/Artwork.cs
  21. 6
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  22. 5908
      ErsatzTV.Infrastructure.MySql/Migrations/20250701110522_Add_ArtworkOriginalContentType.Designer.cs
  23. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20250701110522_Add_ArtworkOriginalContentType.cs
  24. 5911
      ErsatzTV.Infrastructure.MySql/Migrations/20250701115339_Add_ChannelWatermarkOriginalContentType.Designer.cs
  25. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20250701115339_Add_ChannelWatermarkOriginalContentType.cs
  26. 6
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  27. 5747
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250701110542_Add_ArtworkOriginalContentType.Designer.cs
  28. 28
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250701110542_Add_ArtworkOriginalContentType.cs
  29. 5750
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250701115311_Add_ChannelWatermarkOriginalContentType.Designer.cs
  30. 28
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250701115311_Add_ChannelWatermarkOriginalContentType.cs
  31. 6
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  32. 10
      ErsatzTV/Controllers/ArtworkController.cs
  33. 4
      ErsatzTV/Controllers/IptvController.cs
  34. 14
      ErsatzTV/Pages/ChannelEditor.razor
  35. 4
      ErsatzTV/Pages/Channels.razor
  36. 11
      ErsatzTV/Pages/WatermarkEditor.razor
  37. 4
      ErsatzTV/Pages/Watermarks.razor
  38. 9
      ErsatzTV/ViewModels/ChannelEditViewModel.cs
  39. 5
      ErsatzTV/ViewModels/WatermarkEditViewModel.cs

4
CHANGELOG.md

@ -69,6 +69,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -69,6 +69,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix audio sync issue with QSV acceleration
- YAML playout: fix history for marathon and playlist content
- This allows playouts to be extended correctly, instead of always resetting to the earliest item in each group
- Fix using channel External Logo URL as watermark
- Fix display of SVG channel logo and watermark in admin UI
- Existing SVG logos and watermarks will have to be re-uploaded to display properly in the admin UI
- This does not affect streaming at all; existing artwork still works fine for streaming
## [25.2.0] - 2025-06-24
### Added

17
ErsatzTV.Application/Artworks/ArtworkContentTypeModel.cs

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
using System.Net;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Artworks;
public record ArtworkContentTypeModel(string Path, string ContentType)
{
public static readonly ArtworkContentTypeModel None = new(string.Empty, string.Empty);
public bool IsExternalUrl => Artwork.IsExternalUrl(Path);
public bool HasContentType => !string.IsNullOrWhiteSpace(ContentType);
public string UrlWithContentType => string.IsNullOrWhiteSpace(ContentType)
? Path
: $"{Path}?contentType={WebUtility.UrlEncode(ContentType)}";
}

3
ErsatzTV.Application/Channels/ChannelViewModel.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using ErsatzTV.Core.Domain;
using System.Net;
using ErsatzTV.Application.Artworks;
namespace ErsatzTV.Application.Channels;
@ -10,7 +11,7 @@ public record ChannelViewModel( @@ -10,7 +11,7 @@ public record ChannelViewModel(
string Group,
string Categories,
int FFmpegProfileId,
string Logo,
ArtworkContentTypeModel Logo,
ChannelStreamSelectorMode StreamSelectorMode,
string StreamSelector,
string PreferredAudioLanguageCode,

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

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core;
using ErsatzTV.Application.Artworks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Channels;
@ -9,7 +10,7 @@ public record CreateChannel( @@ -9,7 +10,7 @@ public record CreateChannel(
string Group,
string Categories,
int FFmpegProfileId,
string Logo,
ArtworkContentTypeModel Logo,
ChannelStreamSelectorMode StreamSelectorMode,
string StreamSelector,
string PreferredAudioLanguageCode,

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

@ -49,9 +49,9 @@ public class CreateChannelHandler( @@ -49,9 +49,9 @@ public class CreateChannelHandler(
fillerPresetId) =>
{
var artwork = new List<Artwork>();
if (!string.IsNullOrWhiteSpace(request.Logo))
if (!string.IsNullOrWhiteSpace(request.Logo?.Path))
{
string logo = request.Logo;
string logo = request.Logo.Path;
if (logo.StartsWith("iptv/logos/", StringComparison.Ordinal))
{
logo = logo.Replace("iptv/logos/", string.Empty);
@ -62,6 +62,7 @@ public class CreateChannelHandler( @@ -62,6 +62,7 @@ public class CreateChannelHandler(
{
Path = logo,
ArtworkKind = ArtworkKind.Logo,
OriginalContentType = !string.IsNullOrEmpty(request.Logo.ContentType) ? request.Logo.ContentType : null,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
});

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

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core;
using ErsatzTV.Application.Artworks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Channels;
@ -10,7 +11,7 @@ public record UpdateChannel( @@ -10,7 +11,7 @@ public record UpdateChannel(
string Group,
string Categories,
int FFmpegProfileId,
string Logo,
ArtworkContentTypeModel Logo,
ChannelStreamSelectorMode StreamSelectorMode,
string StreamSelector,
string PreferredAudioLanguageCode,

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

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using System.Globalization;
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
using System.Threading.Channels;
using ErsatzTV.Application.Subtitles;
using ErsatzTV.Core;
@ -47,9 +46,9 @@ public class UpdateChannelHandler( @@ -47,9 +46,9 @@ public class UpdateChannelHandler(
c.ActiveMode = update.ActiveMode;
c.Artwork ??= [];
if (!string.IsNullOrWhiteSpace(update.Logo))
if (!string.IsNullOrWhiteSpace(update.Logo?.Path))
{
string logo = update.Logo;
string logo = update.Logo.Path;
if (logo.StartsWith("iptv/logos/", StringComparison.Ordinal))
{
logo = logo.Replace("iptv/logos/", string.Empty);
@ -59,6 +58,9 @@ public class UpdateChannelHandler( @@ -59,6 +58,9 @@ public class UpdateChannelHandler(
foreach (Artwork artwork in maybeLogo)
{
artwork.Path = logo;
artwork.OriginalContentType = !string.IsNullOrEmpty(update.Logo.ContentType)
? update.Logo.ContentType
: null;
artwork.DateUpdated = DateTime.UtcNow;
}
@ -67,6 +69,9 @@ public class UpdateChannelHandler( @@ -67,6 +69,9 @@ public class UpdateChannelHandler(
var artwork = new Artwork
{
Path = logo,
OriginalContentType = !string.IsNullOrEmpty(update.Logo.ContentType)
? update.Logo.ContentType
: null,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow,
ArtworkKind = ArtworkKind.Logo

11
ErsatzTV.Application/Channels/Mapper.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core.Api.Channels;
using ErsatzTV.Application.Artworks;
using ErsatzTV.Core.Api.Channels;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Channels;
@ -45,7 +46,7 @@ internal static class Mapper @@ -45,7 +46,7 @@ internal static class Mapper
internal static ResolutionAndBitrateViewModel ProjectToViewModel(Resolution resolution, int bitrate) =>
new(resolution.Height, resolution.Width, bitrate);
private static string GetLogo(Channel channel)
private static ArtworkContentTypeModel GetLogo(Channel channel)
{
Option<Artwork> maybeArtwork = channel.Artwork
.Where(a => a.ArtworkKind == ArtworkKind.Logo)
@ -53,10 +54,12 @@ internal static class Mapper @@ -53,10 +54,12 @@ internal static class Mapper
foreach (Artwork artwork in maybeArtwork)
{
return artwork.IsExternalUrl() ? artwork.Path : $"iptv/logos/{artwork.Path}";
return artwork.IsExternalUrl()
? new ArtworkContentTypeModel(artwork.Path, string.Empty)
: new ArtworkContentTypeModel($"iptv/logos/{artwork.Path}", artwork.OriginalContentType);
}
return string.Empty;
return ArtworkContentTypeModel.None;
}
private static string GetStreamingMode(Channel channel) =>

2
ErsatzTV.Application/Images/Commands/SaveArtworkToDisk.cs

@ -4,4 +4,4 @@ using ErsatzTV.Core.Domain; @@ -4,4 +4,4 @@ using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Images;
// ReSharper disable once SuggestBaseTypeForParameter
public record SaveArtworkToDisk(Stream Stream, ArtworkKind ArtworkKind) : IRequest<Either<BaseError, string>>;
public record SaveArtworkToDisk(Stream Stream, ArtworkKind ArtworkKind, string ContentType) : IRequest<Either<BaseError, string>>;

2
ErsatzTV.Application/Images/Queries/GetCachedImagePath.cs

@ -3,5 +3,5 @@ using ErsatzTV.Core.Domain; @@ -3,5 +3,5 @@ using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Images;
public record GetCachedImagePath(string FileName, ArtworkKind ArtworkKind, int? MaxHeight = null) : IRequest<
public record GetCachedImagePath(string FileName, ArtworkKind ArtworkKind, string ContentType, int? MaxHeight = null) : IRequest<
Either<BaseError, CachedImagePathViewModel>>;

10
ErsatzTV.Application/Images/Queries/GetCachedImagePathHandler.cs

@ -42,7 +42,7 @@ public class @@ -42,7 +42,7 @@ public class
{
try
{
MimeType mimeType;
string mimeType;
string cachePath = _imageCache.GetPathForImage(
request.FileName,
@ -84,7 +84,7 @@ public class @@ -84,7 +84,7 @@ public class
File.Move(withExtension, cachePath);
mimeType = new MimeType("image/jpeg");
mimeType = "image/jpeg";
}
else
{
@ -93,10 +93,12 @@ public class @@ -93,10 +93,12 @@ public class
}
else
{
mimeType = MimeTypes.GetMimeTypeFromFile(cachePath);
mimeType = !string.IsNullOrWhiteSpace(request.ContentType)
? request.ContentType
: MimeTypes.GetMimeTypeFromFile(cachePath).Name;
}
return new CachedImagePathViewModel(cachePath, mimeType.Name);
return new CachedImagePathViewModel(cachePath, mimeType);
}
catch (Exception ex)
{

5
ErsatzTV.Application/Watermarks/Commands/CreateWatermark.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core;
using ErsatzTV.Application.Artworks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.FFmpeg.State;
@ -6,7 +7,7 @@ namespace ErsatzTV.Application.Watermarks; @@ -6,7 +7,7 @@ namespace ErsatzTV.Application.Watermarks;
public record CreateWatermark(
string Name,
string Image,
ArtworkContentTypeModel Image,
ChannelWatermarkMode Mode,
ChannelWatermarkImageSource ImageSource,
WatermarkLocation Location,

40
ErsatzTV.Application/Watermarks/Commands/CreateWatermarkHandler.cs

@ -39,21 +39,33 @@ public class CreateWatermarkHandler : IRequestHandler<CreateWatermark, Either<Ba @@ -39,21 +39,33 @@ public class CreateWatermarkHandler : IRequestHandler<CreateWatermark, Either<Ba
private static Validation<BaseError, ChannelWatermark> Validate(CreateWatermark request) =>
ValidateName(request)
.Map(
_ => new ChannelWatermark
_ =>
{
Name = request.Name,
Image = request.ImageSource == ChannelWatermarkImageSource.Custom ? request.Image : null,
Mode = request.Mode,
ImageSource = request.ImageSource,
Location = request.Location,
Size = request.Size,
WidthPercent = request.Width,
HorizontalMarginPercent = request.HorizontalMargin,
VerticalMarginPercent = request.VerticalMargin,
FrequencyMinutes = request.FrequencyMinutes,
DurationSeconds = request.DurationSeconds,
Opacity = request.Opacity,
PlaceWithinSourceContent = request.PlaceWithinSourceContent
var watermark = new ChannelWatermark
{
Name = request.Name,
Image = null,
OriginalContentType = null,
Mode = request.Mode,
ImageSource = request.ImageSource,
Location = request.Location,
Size = request.Size,
WidthPercent = request.Width,
HorizontalMarginPercent = request.HorizontalMargin,
VerticalMarginPercent = request.VerticalMargin,
FrequencyMinutes = request.FrequencyMinutes,
DurationSeconds = request.DurationSeconds,
Opacity = request.Opacity,
PlaceWithinSourceContent = request.PlaceWithinSourceContent
};
if (request.ImageSource == ChannelWatermarkImageSource.Custom)
{
watermark.Image = request.Image?.Path;
watermark.OriginalContentType = request.Image?.ContentType;
}
return watermark;
});
private static Validation<BaseError, string> ValidateName(CreateWatermark request) =>

5
ErsatzTV.Application/Watermarks/Commands/UpdateWatermark.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core;
using ErsatzTV.Application.Artworks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.FFmpeg.State;
@ -7,7 +8,7 @@ namespace ErsatzTV.Application.Watermarks; @@ -7,7 +8,7 @@ namespace ErsatzTV.Application.Watermarks;
public record UpdateWatermark(
int Id,
string Name,
string Image,
ArtworkContentTypeModel Image,
ChannelWatermarkMode Mode,
ChannelWatermarkImageSource ImageSource,
WatermarkLocation Location,

10
ErsatzTV.Application/Watermarks/Commands/UpdateWatermarkHandler.cs

@ -33,7 +33,15 @@ public class UpdateWatermarkHandler : IRequestHandler<UpdateWatermark, Either<Ba @@ -33,7 +33,15 @@ public class UpdateWatermarkHandler : IRequestHandler<UpdateWatermark, Either<Ba
UpdateWatermark update)
{
p.Name = update.Name;
p.Image = update.ImageSource == ChannelWatermarkImageSource.Custom ? update.Image : null;
p.Image = null;
p.OriginalContentType = null;
if (update.ImageSource == ChannelWatermarkImageSource.Custom)
{
p.Image = update.Image?.Path;
p.OriginalContentType = update.Image?.ContentType;
}
p.Mode = update.Mode;
p.ImageSource = update.ImageSource;
p.Location = update.Location;

5
ErsatzTV.Application/Watermarks/Mapper.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Application.Artworks;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Watermarks;
@ -7,7 +8,7 @@ internal static class Mapper @@ -7,7 +8,7 @@ internal static class Mapper
public static WatermarkViewModel ProjectToViewModel(ChannelWatermark watermark) =>
new(
watermark.Id,
watermark.Image,
new ArtworkContentTypeModel(watermark.Image, watermark.OriginalContentType),
watermark.Name,
watermark.Mode,
watermark.ImageSource,

2
ErsatzTV.Application/Watermarks/Queries/GetWatermarkByIdHandler.cs

@ -16,7 +16,7 @@ public class GetWatermarkByIdHandler : IRequestHandler<GetWatermarkById, Option< @@ -16,7 +16,7 @@ public class GetWatermarkByIdHandler : IRequestHandler<GetWatermarkById, Option<
GetWatermarkById request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.ChannelWatermarks
.SelectOneAsync(w => w.Id, w => w.Id == request.Id)
.MapT(ProjectToViewModel);

5
ErsatzTV.Application/Watermarks/WatermarkViewModel.cs

@ -1,11 +1,12 @@ @@ -1,11 +1,12 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Application.Artworks;
using ErsatzTV.Core.Domain;
using ErsatzTV.FFmpeg.State;
namespace ErsatzTV.Application.Watermarks;
public record WatermarkViewModel(
int Id,
string Image,
ArtworkContentTypeModel Image,
string Name,
ChannelWatermarkMode Mode,
ChannelWatermarkImageSource ImageSource,

1
ErsatzTV.Core/Domain/ChannelWatermark.cs

@ -9,6 +9,7 @@ public class ChannelWatermark @@ -9,6 +9,7 @@ public class ChannelWatermark
public ChannelWatermarkMode Mode { get; set; }
public ChannelWatermarkImageSource ImageSource { get; set; }
public string Image { get; set; }
public string OriginalContentType { get; set; }
public WatermarkLocation Location { get; set; }
public WatermarkSize Size { get; set; }
public int WidthPercent { get; set; }

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

@ -8,6 +8,7 @@ public class Artwork @@ -8,6 +8,7 @@ public class Artwork
public string BlurHash43 { get; set; }
public string BlurHash54 { get; set; }
public string BlurHash64 { get; set; }
public string OriginalContentType { get; set; }
public ArtworkKind ArtworkKind { get; set; }
public DateTime DateAdded { get; set; }
public DateTime DateUpdated { get; set; }

6
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -205,7 +205,7 @@ public class FFmpegProcessService @@ -205,7 +205,7 @@ public class FFmpegProcessService
channel.Artwork
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
.HeadOrNone()
.Map(a => _imageCache.GetPathForImage(a.Path, ArtworkKind.Logo, Option<int>.None));
.Map(a => Artwork.IsExternalUrl(a.Path) ? a.Path : _imageCache.GetPathForImage(a.Path, ArtworkKind.Logo, Option<int>.None));
return new WatermarkOptions(
await watermarkOverride.IfNoneAsync(watermark),
@ -242,7 +242,7 @@ public class FFmpegProcessService @@ -242,7 +242,7 @@ public class FFmpegProcessService
channel.Artwork
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
.HeadOrNone()
.Map(a => _imageCache.GetPathForImage(a.Path, ArtworkKind.Logo, Option<int>.None));
.Map(a => Artwork.IsExternalUrl(a.Path) ? a.Path : _imageCache.GetPathForImage(a.Path, ArtworkKind.Logo, Option<int>.None));
return new WatermarkOptions(
await watermarkOverride.IfNoneAsync(channel.Watermark),
maybeChannelPath,
@ -278,7 +278,7 @@ public class FFmpegProcessService @@ -278,7 +278,7 @@ public class FFmpegProcessService
channel.Artwork
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
.HeadOrNone()
.Map(a => _imageCache.GetPathForImage(a.Path, ArtworkKind.Logo, Option<int>.None));
.Map(a => Artwork.IsExternalUrl(a.Path) ? a.Path : _imageCache.GetPathForImage(a.Path, ArtworkKind.Logo, Option<int>.None));
return new WatermarkOptions(
await watermarkOverride.IfNoneAsync(watermark),
maybeChannelPath,

5908
ErsatzTV.Infrastructure.MySql/Migrations/20250701110522_Add_ArtworkOriginalContentType.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20250701110522_Add_ArtworkOriginalContentType.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_ArtworkOriginalContentType : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "OriginalContentType",
table: "Artwork",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "OriginalContentType",
table: "Artwork");
}
}
}

5911
ErsatzTV.Infrastructure.MySql/Migrations/20250701115339_Add_ChannelWatermarkOriginalContentType.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20250701115339_Add_ChannelWatermarkOriginalContentType.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_ChannelWatermarkOriginalContentType : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "OriginalContentType",
table: "ChannelWatermark",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "OriginalContentType",
table: "ChannelWatermark");
}
}
}

6
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -190,6 +190,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -190,6 +190,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int?>("MusicVideoMetadataId")
.HasColumnType("int");
b.Property<string>("OriginalContentType")
.HasColumnType("longtext");
b.Property<int?>("OtherVideoMetadataId")
.HasColumnType("int");
@ -353,6 +356,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -353,6 +356,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int>("Opacity")
.HasColumnType("int");
b.Property<string>("OriginalContentType")
.HasColumnType("longtext");
b.Property<bool>("PlaceWithinSourceContent")
.HasColumnType("tinyint(1)");

5747
ErsatzTV.Infrastructure.Sqlite/Migrations/20250701110542_Add_ArtworkOriginalContentType.Designer.cs generated

File diff suppressed because it is too large Load Diff

28
ErsatzTV.Infrastructure.Sqlite/Migrations/20250701110542_Add_ArtworkOriginalContentType.cs

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_ArtworkOriginalContentType : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "OriginalContentType",
table: "Artwork",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "OriginalContentType",
table: "Artwork");
}
}
}

5750
ErsatzTV.Infrastructure.Sqlite/Migrations/20250701115311_Add_ChannelWatermarkOriginalContentType.Designer.cs generated

File diff suppressed because it is too large Load Diff

28
ErsatzTV.Infrastructure.Sqlite/Migrations/20250701115311_Add_ChannelWatermarkOriginalContentType.cs

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_ChannelWatermarkOriginalContentType : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "OriginalContentType",
table: "ChannelWatermark",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "OriginalContentType",
table: "ChannelWatermark");
}
}
}

6
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -179,6 +179,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -179,6 +179,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int?>("MusicVideoMetadataId")
.HasColumnType("INTEGER");
b.Property<string>("OriginalContentType")
.HasColumnType("TEXT");
b.Property<int?>("OtherVideoMetadataId")
.HasColumnType("INTEGER");
@ -338,6 +341,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -338,6 +341,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("Opacity")
.HasColumnType("INTEGER");
b.Property<string>("OriginalContentType")
.HasColumnType("TEXT");
b.Property<bool>("PlaceWithinSourceContent")
.HasColumnType("INTEGER");

10
ErsatzTV/Controllers/ArtworkController.cs

@ -63,17 +63,17 @@ public class ArtworkController : ControllerBase @@ -63,17 +63,17 @@ public class ArtworkController : ControllerBase
public async Task<IActionResult> GetPoster(string fileName, CancellationToken cancellationToken)
{
Either<BaseError, CachedImagePathViewModel> cachedImagePath =
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Poster, 440), cancellationToken);
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Poster, string.Empty, 440), cancellationToken);
return cachedImagePath.Match<IActionResult>(
Left: _ => new NotFoundResult(),
Right: r => new PhysicalFileResult(r.FileName, r.MimeType));
}
[HttpGet("/artwork/watermarks/{fileName}")]
public async Task<IActionResult> GetWatermark(string fileName, CancellationToken cancellationToken)
public async Task<IActionResult> GetWatermark(string fileName, [FromQuery] string contentType, CancellationToken cancellationToken)
{
Either<BaseError, CachedImagePathViewModel> cachedImagePath =
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Watermark), cancellationToken);
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Watermark, contentType), cancellationToken);
return cachedImagePath.Match<IActionResult>(
Left: _ => new NotFoundResult(),
Right: r => new PhysicalFileResult(r.FileName, r.MimeType));
@ -83,7 +83,7 @@ public class ArtworkController : ControllerBase @@ -83,7 +83,7 @@ public class ArtworkController : ControllerBase
public async Task<IActionResult> GetFanArt(string fileName, CancellationToken cancellationToken)
{
Either<BaseError, CachedImagePathViewModel> cachedImagePath =
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.FanArt), cancellationToken);
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.FanArt, string.Empty), cancellationToken);
return cachedImagePath.Match<IActionResult>(
Left: _ => new NotFoundResult(),
Right: r => new PhysicalFileResult(r.FileName, r.MimeType));
@ -155,7 +155,7 @@ public class ArtworkController : ControllerBase @@ -155,7 +155,7 @@ public class ArtworkController : ControllerBase
public async Task<IActionResult> GetThumbnail(string fileName, CancellationToken cancellationToken)
{
Either<BaseError, CachedImagePathViewModel> cachedImagePath =
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Thumbnail, 220), cancellationToken);
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Thumbnail, string.Empty, 220), cancellationToken);
return cachedImagePath.Match<IActionResult>(
Left: _ => new NotFoundResult(),
Right: r => new PhysicalFileResult(r.FileName, r.MimeType));

4
ErsatzTV/Controllers/IptvController.cs

@ -271,10 +271,10 @@ public class IptvController : ControllerBase @@ -271,10 +271,10 @@ public class IptvController : ControllerBase
[HttpGet("iptv/logos/{fileName}")]
[HttpHead("iptv/logos/{fileName}.jpg")]
[HttpGet("iptv/logos/{fileName}.jpg")]
public async Task<IActionResult> GetImage(string fileName)
public async Task<IActionResult> GetImage(string fileName, [FromQuery] string contentType)
{
Either<BaseError, CachedImagePathViewModel> cachedImagePath =
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Logo));
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Logo, contentType));
return cachedImagePath.Match<IActionResult>(
Left: _ => new NotFoundResult(),
Right: r => new PhysicalFileResult(r.FileName, r.MimeType));

14
ErsatzTV/Pages/ChannelEditor.razor

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
@page "/channels/{Id:int?}"
@page "/channels/add"
@using System.Net
@using ErsatzTV.Application.Artworks
@using ErsatzTV.Application.Channels
@using ErsatzTV.Application.FFmpegProfiles
@using ErsatzTV.Application.Filler
@ -124,9 +126,9 @@ @@ -124,9 +126,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) || !string.IsNullOrWhiteSpace(_model.ExternalLogoUrl))
@if (!string.IsNullOrWhiteSpace(_model.Logo?.Path) || !string.IsNullOrWhiteSpace(_model.ExternalLogoUrl))
{
<MudElement HtmlTag="img" src="@(string.IsNullOrWhiteSpace(_model.ExternalLogoUrl) ? _model.Logo : _model.ExternalLogoUrl)" Style="max-height: 50px"/>
<MudElement HtmlTag="img" src="@(string.IsNullOrWhiteSpace(_model.ExternalLogoUrl) ? _model.Logo.UrlWithContentType : _model.ExternalLogoUrl)" Style="max-height: 50px"/>
}
</MudItem>
<MudItem xs="6">
@ -216,9 +218,9 @@ @@ -216,9 +218,9 @@
_model.Number = channelViewModel.Number;
_model.FFmpegProfileId = channelViewModel.FFmpegProfileId;
if (Artwork.IsExternalUrl(channelViewModel.Logo))
if (channelViewModel.Logo.IsExternalUrl)
{
_model.ExternalLogoUrl = channelViewModel.Logo;
_model.ExternalLogoUrl = channelViewModel.Logo.Path;
}
else
{
@ -304,11 +306,11 @@ @@ -304,11 +306,11 @@
try
{
Either<BaseError, string> maybeCacheFileName =
await Mediator.Send(new SaveArtworkToDisk(e.File.OpenReadStream(10 * 1024 * 1024), ArtworkKind.Logo), _cts.Token);
await Mediator.Send(new SaveArtworkToDisk(e.File.OpenReadStream(10 * 1024 * 1024), ArtworkKind.Logo, e.File.ContentType), _cts.Token);
maybeCacheFileName.Match(
relativeFileName =>
{
_model.Logo = $"iptv/logos/{relativeFileName}";
_model.Logo = new ArtworkContentTypeModel($"iptv/logos/{relativeFileName}", e.File.ContentType);
_model.ExternalLogoUrl = null;
StateHasChanged();
},

4
ErsatzTV/Pages/Channels.razor

@ -43,9 +43,9 @@ @@ -43,9 +43,9 @@
<RowTemplate>
<MudTd DataLabel="Number">@context.Number</MudTd>
<MudTd DataLabel="Logo">
@if (!string.IsNullOrWhiteSpace(context.Logo))
@if (!string.IsNullOrWhiteSpace(context.Logo?.Path))
{
<MudElement HtmlTag="img" src="@context.Logo" Style="max-height: 50px"/>
<MudElement HtmlTag="img" src="@context.Logo.UrlWithContentType" Style="max-height: 50px"/>
}
else
{

11
ErsatzTV/Pages/WatermarkEditor.razor

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
@page "/watermarks/{Id:int}"
@page "/watermarks/add"
@using ErsatzTV.Application.Artworks
@using ErsatzTV.Application.Images
@using ErsatzTV.Application.Watermarks
@using ErsatzTV.FFmpeg.State
@ -38,9 +39,9 @@ @@ -38,9 +39,9 @@
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
<MudItem xs="6">
<InputFile id="watermarkFileInput" OnChange="UploadWatermark" style="display: none;"/>
@if (!string.IsNullOrWhiteSpace(_model.Image) && _model.ImageSource == ChannelWatermarkImageSource.Custom)
@if (!string.IsNullOrWhiteSpace(_model.Image?.Path) && _model.ImageSource == ChannelWatermarkImageSource.Custom)
{
<MudElement HtmlTag="img" src="@($"artwork/watermarks/{_model.Image}")" Style="max-height: 50px"/>
<MudElement HtmlTag="img" src="@($"artwork/watermarks/{_model.Image.UrlWithContentType}")" Style="max-height: 50px"/>
}
<ValidationMessage For="@(() => _model.Image)" style="color: #f44336 !important;"/>
</MudItem>
@ -183,7 +184,7 @@ @@ -183,7 +184,7 @@
{
Name = string.Empty,
Mode = ChannelWatermarkMode.Permanent,
Image = string.Empty,
Image = ArtworkContentTypeModel.None,
Location = WatermarkLocation.BottomRight,
Size = WatermarkSize.Scaled,
Width = 15,
@ -224,12 +225,12 @@ @@ -224,12 +225,12 @@
try
{
Either<BaseError, string> maybeCacheFileName = await Mediator.Send(
new SaveArtworkToDisk(e.File.OpenReadStream(10 * 1024 * 1024), ArtworkKind.Watermark),
new SaveArtworkToDisk(e.File.OpenReadStream(10 * 1024 * 1024), ArtworkKind.Watermark, e.File.ContentType),
_cts.Token);
maybeCacheFileName.Match(
relativeFileName =>
{
_model.Image = relativeFileName;
_model.Image = new ArtworkContentTypeModel(relativeFileName, e.File.ContentType);
_messageStore.Clear();
_editContext.Validate();
StateHasChanged();

4
ErsatzTV/Pages/Watermarks.razor

@ -27,9 +27,9 @@ @@ -27,9 +27,9 @@
<RowTemplate>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd DataLabel="Image">
@if (!string.IsNullOrWhiteSpace(context.Image))
@if (!string.IsNullOrWhiteSpace(context.Image?.Path))
{
<MudElement HtmlTag="img" src="@($"artwork/watermarks/{context.Image}")" Style="max-height: 50px"/>
<MudElement HtmlTag="img" src="@($"artwork/watermarks/{context.Image.UrlWithContentType}")" Style="max-height: 50px"/>
}
else if (context.ImageSource == ChannelWatermarkImageSource.ChannelLogo)
{

9
ErsatzTV/ViewModels/ChannelEditViewModel.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Application.Channels;
using ErsatzTV.Application.Artworks;
using ErsatzTV.Application.Channels;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.ViewModels;
@ -16,7 +17,7 @@ public class ChannelEditViewModel @@ -16,7 +17,7 @@ public class ChannelEditViewModel
public string StreamSelector { get; set; }
public string PreferredAudioLanguageCode { get; set; }
public string PreferredAudioTitle { get; set; }
public string Logo { get; set; }
public ArtworkContentTypeModel Logo { get; set; }
public string ExternalLogoUrl { get; set; }
public ChannelProgressMode ProgressMode { get; set; }
public StreamingMode StreamingMode { get; set; }
@ -43,7 +44,7 @@ public class ChannelEditViewModel @@ -43,7 +44,7 @@ public class ChannelEditViewModel
Group,
Categories,
FFmpegProfileId,
string.IsNullOrWhiteSpace(ExternalLogoUrl) ? Logo : ExternalLogoUrl,
string.IsNullOrWhiteSpace(ExternalLogoUrl) ? Logo : new ArtworkContentTypeModel(ExternalLogoUrl, string.Empty),
StreamSelectorMode,
StreamSelector,
PreferredAudioLanguageCode,
@ -66,7 +67,7 @@ public class ChannelEditViewModel @@ -66,7 +67,7 @@ public class ChannelEditViewModel
Group,
Categories,
FFmpegProfileId,
string.IsNullOrWhiteSpace(ExternalLogoUrl) ? Logo : ExternalLogoUrl,
string.IsNullOrWhiteSpace(ExternalLogoUrl) ? Logo : new ArtworkContentTypeModel(ExternalLogoUrl, string.Empty),
StreamSelectorMode,
StreamSelector,
PreferredAudioLanguageCode,

5
ErsatzTV/ViewModels/WatermarkEditViewModel.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Application.Artworks;
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Core.Domain;
using ErsatzTV.FFmpeg.State;
@ -30,7 +31,7 @@ public class WatermarkEditViewModel @@ -30,7 +31,7 @@ public class WatermarkEditViewModel
public int Id { get; set; }
public string Name { get; set; }
public string Image { get; set; }
public ArtworkContentTypeModel Image { get; set; }
public ChannelWatermarkMode Mode { get; set; }
public ChannelWatermarkImageSource ImageSource { get; set; }
public WatermarkLocation Location { get; set; }

Loading…
Cancel
Save