channel logo and watermark fixes (#2100)
* channel logo and watermark fixes * update changelog
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)}";
|
||||
}
|
||||
@@ -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(
|
||||
string Group,
|
||||
string Categories,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
ArtworkContentTypeModel Logo,
|
||||
ChannelStreamSelectorMode StreamSelectorMode,
|
||||
string StreamSelector,
|
||||
string PreferredAudioLanguageCode,
|
||||
|
||||
@@ -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(
|
||||
string Group,
|
||||
string Categories,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
ArtworkContentTypeModel Logo,
|
||||
ChannelStreamSelectorMode StreamSelectorMode,
|
||||
string StreamSelector,
|
||||
string PreferredAudioLanguageCode,
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
Path = logo,
|
||||
ArtworkKind = ArtworkKind.Logo,
|
||||
OriginalContentType = !string.IsNullOrEmpty(request.Logo.ContentType) ? request.Logo.ContentType : null,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
DateUpdated = DateTime.UtcNow
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
string Group,
|
||||
string Categories,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
ArtworkContentTypeModel Logo,
|
||||
ChannelStreamSelectorMode StreamSelectorMode,
|
||||
string StreamSelector,
|
||||
string PreferredAudioLanguageCode,
|
||||
|
||||
@@ -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(
|
||||
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(
|
||||
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(
|
||||
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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
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) =>
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -42,7 +42,7 @@ public class
|
||||
{
|
||||
try
|
||||
{
|
||||
MimeType mimeType;
|
||||
string mimeType;
|
||||
|
||||
string cachePath = _imageCache.GetPathForImage(
|
||||
request.FileName,
|
||||
@@ -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
|
||||
}
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
public record CreateWatermark(
|
||||
string Name,
|
||||
string Image,
|
||||
ArtworkContentTypeModel Image,
|
||||
ChannelWatermarkMode Mode,
|
||||
ChannelWatermarkImageSource ImageSource,
|
||||
WatermarkLocation Location,
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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;
|
||||
public record UpdateWatermark(
|
||||
int Id,
|
||||
string Name,
|
||||
string Image,
|
||||
ArtworkContentTypeModel Image,
|
||||
ChannelWatermarkMode Mode,
|
||||
ChannelWatermarkImageSource ImageSource,
|
||||
WatermarkLocation Location,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
public static WatermarkViewModel ProjectToViewModel(ChannelWatermark watermark) =>
|
||||
new(
|
||||
watermark.Id,
|
||||
watermark.Image,
|
||||
new ArtworkContentTypeModel(watermark.Image, watermark.OriginalContentType),
|
||||
watermark.Name,
|
||||
watermark.Mode,
|
||||
watermark.ImageSource,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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,
|
||||
|
||||
Generated
+5908
File diff suppressed because it is too large
Load Diff
+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
File diff suppressed because it is too large
Load Diff
+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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
b.Property<int>("Opacity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("OriginalContentType")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<bool>("PlaceWithinSourceContent")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
|
||||
Generated
+5747
File diff suppressed because it is too large
Load Diff
+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
File diff suppressed because it is too large
Load Diff
+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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
b.Property<int>("Opacity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("OriginalContentType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PlaceWithinSourceContent")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 @@
|
||||
<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 @@
|
||||
_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 @@
|
||||
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();
|
||||
},
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 @@
|
||||
<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 @@
|
||||
{
|
||||
Name = string.Empty,
|
||||
Mode = ChannelWatermarkMode.Permanent,
|
||||
Image = string.Empty,
|
||||
Image = ArtworkContentTypeModel.None,
|
||||
Location = WatermarkLocation.BottomRight,
|
||||
Size = WatermarkSize.Scaled,
|
||||
Width = 15,
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
Group,
|
||||
Categories,
|
||||
FFmpegProfileId,
|
||||
string.IsNullOrWhiteSpace(ExternalLogoUrl) ? Logo : ExternalLogoUrl,
|
||||
string.IsNullOrWhiteSpace(ExternalLogoUrl) ? Logo : new ArtworkContentTypeModel(ExternalLogoUrl, string.Empty),
|
||||
StreamSelectorMode,
|
||||
StreamSelector,
|
||||
PreferredAudioLanguageCode,
|
||||
|
||||
@@ -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
|
||||
|
||||
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; }
|
||||
|
||||
Reference in New Issue
Block a user