Browse Source

add channel mirror (#2390)

* add channel playout source (doesn't do anything yet)

* configure mirror channel

* fix mirror playback

* sync epg for mirror channel

* update changelog
pull/2391/head
Jason Dove 4 months ago committed by GitHub
parent
commit
5e7da19e5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 2
      ErsatzTV.Application/Channels/ChannelViewModel.cs
  3. 2
      ErsatzTV.Application/Channels/Commands/CreateChannel.cs
  4. 11
      ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
  5. 15
      ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs
  6. 2
      ErsatzTV.Application/Channels/Commands/UpdateChannel.cs
  7. 22
      ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
  8. 6
      ErsatzTV.Application/Channels/Mapper.cs
  9. 22
      ErsatzTV.Application/Channels/Queries/GetAllChannelsHandler.cs
  10. 2
      ErsatzTV.Application/Channels/Queries/GetChannelByIdHandler.cs
  11. 9
      ErsatzTV.Application/Channels/Queries/GetChannelByNumberHandler.cs
  12. 2
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  13. 4
      ErsatzTV.Core/Domain/Channel.cs
  14. 7
      ErsatzTV.Core/Domain/ChannelPlayoutSource.cs
  15. 6384
      ErsatzTV.Infrastructure.MySql/Migrations/20250907150553_Add_ChannelPlayoutSource.Designer.cs
  16. 71
      ErsatzTV.Infrastructure.MySql/Migrations/20250907150553_Add_ChannelPlayoutSource.cs
  17. 18
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  18. 6219
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250907150639_Add_ChannelPlayoutSource.Designer.cs
  19. 71
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250907150639_Add_ChannelPlayoutSource.cs
  20. 18
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  21. 6
      ErsatzTV.Infrastructure/Data/Configurations/ChannelConfiguration.cs
  22. 2
      ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs
  23. 41
      ErsatzTV/Pages/ChannelEditor.razor
  24. 6
      ErsatzTV/ViewModels/ChannelEditViewModel.cs

6
CHANGELOG.md

@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Classic schedules: allow selecting multiple graphics elements on schedule items
- Add channel `Playout Source` setting
- `Generated`: default/existing behavior where channel must have its own playout
- `Mirror`: channel will play content from the specified `Mirror Source Channel`'s playout
- This allows the exact same content on different channels with different channel settings
### Fixed
- Fix transcoding content with bt709/pc color metadata
@ -15,7 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -15,7 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `local_infile=ON` is required when using MySQL (for bulk inserts when building playouts)
- ETV will set this automatically when it has permission
- When ETV does not have permission, startup will fail with logged instructions on how to configure MySql
- Fix scaling content in locales that don't use period as a decimal separator (e.g. `,`)
- Fix scaling anamorphic content in locales that don't use period as a decimal separator (e.g. `,`)
### Changed
- **BREAKING CHANGE**: change how `Scripted Schedule` system works

2
ErsatzTV.Application/Channels/ChannelViewModel.cs

@ -16,7 +16,9 @@ public record ChannelViewModel( @@ -16,7 +16,9 @@ public record ChannelViewModel(
string StreamSelector,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
ChannelPlayoutSource PlayoutSource,
ChannelPlayoutMode PlayoutMode,
int? MirrorSourceChannelId,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,

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

@ -15,7 +15,9 @@ public record CreateChannel( @@ -15,7 +15,9 @@ public record CreateChannel(
string StreamSelector,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
ChannelPlayoutSource PlayoutSource,
ChannelPlayoutMode PlayoutMode,
int? MirrorSourceChannelId,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,

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

@ -76,7 +76,9 @@ public class CreateChannelHandler( @@ -76,7 +76,9 @@ public class CreateChannelHandler(
Group = request.Group,
Categories = request.Categories,
FFmpegProfileId = ffmpegProfileId,
PlayoutSource = request.PlayoutSource,
PlayoutMode = request.PlayoutMode,
MirrorSourceChannelId = request.MirrorSourceChannelId,
StreamingMode = request.StreamingMode,
Artwork = artwork,
StreamSelectorMode = request.StreamSelectorMode,
@ -94,6 +96,15 @@ public class CreateChannelHandler( @@ -94,6 +96,15 @@ public class CreateChannelHandler(
ShowInEpg = request.IsEnabled && request.ShowInEpg
};
if (channel.PlayoutSource is ChannelPlayoutSource.Mirror)
{
channel.PlayoutMode = ChannelPlayoutMode.Continuous;
}
else
{
channel.MirrorSourceChannelId = null;
}
foreach (int id in watermarkId)
{
channel.WatermarkId = id;

15
ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs

@ -52,8 +52,10 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData> @@ -52,8 +52,10 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
string targetFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{request.ChannelNumber}.xml");
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
string channelNumber = request.ChannelNumber;
int hiddenCount = await dbContext.Channels
.Where(c => c.Number == request.ChannelNumber && c.ShowInEpg == false)
.Where(c => c.Number == channelNumber && c.ShowInEpg == false)
.CountAsync(cancellationToken);
if (hiddenCount > 0)
{
@ -97,6 +99,17 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData> @@ -97,6 +99,17 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
string otherVideoText = await File.ReadAllTextAsync(otherVideoTemplateFileName, cancellationToken);
var otherVideoTemplate = Template.Parse(otherVideoText, otherVideoTemplateFileName);
Option<string> maybeMirrorNumber = await dbContext.Channels
.Filter(c => c.Number == channelNumber)
.Filter(c => c.PlayoutSource == ChannelPlayoutSource.Mirror && c.MirrorSourceChannelId != null)
.Map(c => c.MirrorSourceChannel.Number)
.ToListAsync(cancellationToken)
.Map(list => list.HeadOrNone());
foreach (string mirrorNumber in maybeMirrorNumber)
{
request = new RefreshChannelData(ChannelNumber: mirrorNumber);
}
List<Playout> playouts = await dbContext.Playouts
.AsNoTracking()
.Filter(pi => pi.Channel.Number == request.ChannelNumber)

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

@ -16,7 +16,9 @@ public record UpdateChannel( @@ -16,7 +16,9 @@ public record UpdateChannel(
string StreamSelector,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
ChannelPlayoutSource PlayoutSource,
ChannelPlayoutMode PlayoutMode,
int? MirrorSourceChannelId,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,

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

@ -33,6 +33,8 @@ public class UpdateChannelHandler( @@ -33,6 +33,8 @@ public class UpdateChannelHandler(
UpdateChannel update,
CancellationToken cancellationToken)
{
bool hasEpgChange = c.PlayoutSource != update.PlayoutSource || c.ShowInEpg != update.ShowInEpg;
c.Name = update.Name;
c.Number = update.Number;
c.Group = update.Group;
@ -99,10 +101,24 @@ public class UpdateChannelHandler( @@ -99,10 +101,24 @@ public class UpdateChannelHandler(
}
}
c.PlayoutSource = update.PlayoutSource;
c.PlayoutMode = update.PlayoutMode;
if (c.PlayoutSource is ChannelPlayoutSource.Mirror)
{
c.PlayoutMode = ChannelPlayoutMode.Continuous;
hasEpgChange |= c.MirrorSourceChannelId != update.MirrorSourceChannelId;
}
else
{
c.MirrorSourceChannelId = null;
}
c.MirrorSourceChannelId = update.MirrorSourceChannelId;
c.StreamingMode = update.StreamingMode;
c.WatermarkId = update.WatermarkId;
c.FallbackFillerId = update.FallbackFillerId;
await dbContext.SaveChangesAsync(cancellationToken);
searchTargets.SearchTargetsChanged();
@ -119,8 +135,12 @@ public class UpdateChannelHandler( @@ -119,8 +135,12 @@ public class UpdateChannelHandler(
}
await workerChannel.WriteAsync(new RefreshChannelList(), cancellationToken);
if (hasEpgChange)
{
await workerChannel.WriteAsync(new RefreshChannelData(c.Number), cancellationToken);
}
return ProjectToViewModel(c);
return ProjectToViewModel(c, c.Playouts?.Count ?? 0);
}
private static async Task<Validation<BaseError, Channel>> Validate(

6
ErsatzTV.Application/Channels/Mapper.cs

@ -6,7 +6,7 @@ namespace ErsatzTV.Application.Channels; @@ -6,7 +6,7 @@ namespace ErsatzTV.Application.Channels;
internal static class Mapper
{
internal static ChannelViewModel ProjectToViewModel(Channel channel) =>
internal static ChannelViewModel ProjectToViewModel(Channel channel, int playoutCount) =>
new(
channel.Id,
channel.Number,
@ -19,11 +19,13 @@ internal static class Mapper @@ -19,11 +19,13 @@ internal static class Mapper
channel.StreamSelector,
channel.PreferredAudioLanguageCode,
channel.PreferredAudioTitle,
channel.PlayoutSource,
channel.PlayoutMode,
channel.MirrorSourceChannelId,
channel.StreamingMode,
channel.WatermarkId,
channel.FallbackFillerId,
channel.Playouts?.Count ?? 0,
playoutCount,
channel.PreferredSubtitleLanguageCode,
channel.SubtitleMode,
channel.MusicVideoCreditsMode,

22
ErsatzTV.Application/Channels/Queries/GetAllChannelsHandler.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Channels.Mapper;
namespace ErsatzTV.Application.Channels;
@ -8,5 +9,22 @@ public class GetAllChannelsHandler(IChannelRepository channelRepository) @@ -8,5 +9,22 @@ public class GetAllChannelsHandler(IChannelRepository channelRepository)
{
public async Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
await channelRepository.GetAll(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
.Map(list => list.Map(c => ProjectToViewModel(c, GetPlayoutsCount(c))).ToList());
private static int GetPlayoutsCount(Channel channel)
{
var result = 0;
if (channel.Playouts != null)
{
result += channel.Playouts.Count;
}
if (channel.PlayoutSource is ChannelPlayoutSource.Mirror && channel.MirrorSourceChannel?.Playouts != null)
{
result += channel.MirrorSourceChannel.Playouts.Count;
}
return result;
}
}

2
ErsatzTV.Application/Channels/Queries/GetChannelByIdHandler.cs

@ -8,5 +8,5 @@ public class GetChannelByIdHandler(IChannelRepository channelRepository) @@ -8,5 +8,5 @@ public class GetChannelByIdHandler(IChannelRepository channelRepository)
{
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
channelRepository.GetChannel(request.Id)
.MapT(ProjectToViewModel);
.MapT(c => ProjectToViewModel(c, 0));
}

9
ErsatzTV.Application/Channels/Queries/GetChannelByNumberHandler.cs

@ -3,12 +3,9 @@ using static ErsatzTV.Application.Channels.Mapper; @@ -3,12 +3,9 @@ using static ErsatzTV.Application.Channels.Mapper;
namespace ErsatzTV.Application.Channels;
public class GetChannelByNumberHandler : IRequestHandler<GetChannelByNumber, Option<ChannelViewModel>>
public class GetChannelByNumberHandler(IChannelRepository channelRepository)
: IRequestHandler<GetChannelByNumber, Option<ChannelViewModel>>
{
private readonly IChannelRepository _channelRepository;
public GetChannelByNumberHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public Task<Option<ChannelViewModel>> Handle(GetChannelByNumber request, CancellationToken cancellationToken) =>
_channelRepository.GetByNumber(request.ChannelNumber).MapT(ProjectToViewModel);
channelRepository.GetByNumber(request.ChannelNumber).MapT(c => ProjectToViewModel(c, 0));
}

2
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -183,7 +183,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -183,7 +183,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as RemoteStream).RemoteStreamMetadata)
.Include(i => i.Watermarks)
.ForChannelAndTime(channel.Id, now)
.ForChannelAndTime(channel.MirrorSourceChannelId ?? channel.Id, now)
.Map(o => o.ToEither<BaseError>(new UnableToLocatePlayoutItem()))
.BindT(item => ValidatePlayoutItemPath(dbContext, item, cancellationToken));

4
ErsatzTV.Core/Domain/Channel.cs

@ -32,7 +32,11 @@ public class Channel @@ -32,7 +32,11 @@ public class Channel
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
public string MusicVideoCreditsTemplate { get; set; }
public ChannelSongVideoMode SongVideoMode { get; set; }
public ChannelPlayoutSource PlayoutSource { get; set; }
public ChannelPlayoutMode PlayoutMode { get; set; }
public int? MirrorSourceChannelId { get; set; }
public Channel MirrorSourceChannel { get; set; }
public TimeSpan? PlayoutOffset { get; set; }
public ChannelTranscodeMode TranscodeMode { get; set; }
public ChannelIdleBehavior IdleBehavior { get; set; }
public bool IsEnabled { get; set; }

7
ErsatzTV.Core/Domain/ChannelPlayoutSource.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
namespace ErsatzTV.Core.Domain;
public enum ChannelPlayoutSource
{
Generated = 0,
Mirror = 1
}

6384
ErsatzTV.Infrastructure.MySql/Migrations/20250907150553_Add_ChannelPlayoutSource.Designer.cs generated

File diff suppressed because it is too large Load Diff

71
ErsatzTV.Infrastructure.MySql/Migrations/20250907150553_Add_ChannelPlayoutSource.cs

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_ChannelPlayoutSource : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "MirrorSourceChannelId",
table: "Channel",
type: "int",
nullable: true);
migrationBuilder.AddColumn<TimeSpan>(
name: "PlayoutOffset",
table: "Channel",
type: "time(6)",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "PlayoutSource",
table: "Channel",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_Channel_MirrorSourceChannelId",
table: "Channel",
column: "MirrorSourceChannelId");
migrationBuilder.AddForeignKey(
name: "FK_Channel_Channel_MirrorSourceChannelId",
table: "Channel",
column: "MirrorSourceChannelId",
principalTable: "Channel",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Channel_Channel_MirrorSourceChannelId",
table: "Channel");
migrationBuilder.DropIndex(
name: "IX_Channel_MirrorSourceChannelId",
table: "Channel");
migrationBuilder.DropColumn(
name: "MirrorSourceChannelId",
table: "Channel");
migrationBuilder.DropColumn(
name: "PlayoutOffset",
table: "Channel");
migrationBuilder.DropColumn(
name: "PlayoutSource",
table: "Channel");
}
}
}

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

@ -277,6 +277,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -277,6 +277,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
.HasColumnType("tinyint(1)")
.HasDefaultValue(true);
b.Property<int?>("MirrorSourceChannelId")
.HasColumnType("int");
b.Property<int>("MusicVideoCreditsMode")
.HasColumnType("int");
@ -292,6 +295,12 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -292,6 +295,12 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int>("PlayoutMode")
.HasColumnType("int");
b.Property<TimeSpan?>("PlayoutOffset")
.HasColumnType("time(6)");
b.Property<int>("PlayoutSource")
.HasColumnType("int");
b.Property<string>("PreferredAudioLanguageCode")
.HasColumnType("longtext");
@ -336,6 +345,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -336,6 +345,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.HasIndex("FallbackFillerId");
b.HasIndex("MirrorSourceChannelId");
b.HasIndex("Number")
.IsUnique();
@ -4048,6 +4059,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4048,6 +4059,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
.HasForeignKey("FallbackFillerId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ErsatzTV.Core.Domain.Channel", "MirrorSourceChannel")
.WithMany()
.HasForeignKey("MirrorSourceChannelId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
.WithMany()
.HasForeignKey("WatermarkId")
@ -4057,6 +4073,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4057,6 +4073,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("FallbackFiller");
b.Navigation("MirrorSourceChannel");
b.Navigation("Watermark");
});

6219
ErsatzTV.Infrastructure.Sqlite/Migrations/20250907150639_Add_ChannelPlayoutSource.Designer.cs generated

File diff suppressed because it is too large Load Diff

71
ErsatzTV.Infrastructure.Sqlite/Migrations/20250907150639_Add_ChannelPlayoutSource.cs

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_ChannelPlayoutSource : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "MirrorSourceChannelId",
table: "Channel",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<TimeSpan>(
name: "PlayoutOffset",
table: "Channel",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "PlayoutSource",
table: "Channel",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_Channel_MirrorSourceChannelId",
table: "Channel",
column: "MirrorSourceChannelId");
migrationBuilder.AddForeignKey(
name: "FK_Channel_Channel_MirrorSourceChannelId",
table: "Channel",
column: "MirrorSourceChannelId",
principalTable: "Channel",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Channel_Channel_MirrorSourceChannelId",
table: "Channel");
migrationBuilder.DropIndex(
name: "IX_Channel_MirrorSourceChannelId",
table: "Channel");
migrationBuilder.DropColumn(
name: "MirrorSourceChannelId",
table: "Channel");
migrationBuilder.DropColumn(
name: "PlayoutOffset",
table: "Channel");
migrationBuilder.DropColumn(
name: "PlayoutSource",
table: "Channel");
}
}
}

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

@ -264,6 +264,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -264,6 +264,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<int?>("MirrorSourceChannelId")
.HasColumnType("INTEGER");
b.Property<int>("MusicVideoCreditsMode")
.HasColumnType("INTEGER");
@ -279,6 +282,12 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -279,6 +282,12 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("PlayoutMode")
.HasColumnType("INTEGER");
b.Property<TimeSpan?>("PlayoutOffset")
.HasColumnType("TEXT");
b.Property<int>("PlayoutSource")
.HasColumnType("INTEGER");
b.Property<string>("PreferredAudioLanguageCode")
.HasColumnType("TEXT");
@ -323,6 +332,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -323,6 +332,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("FallbackFillerId");
b.HasIndex("MirrorSourceChannelId");
b.HasIndex("Number")
.IsUnique();
@ -3883,6 +3894,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -3883,6 +3894,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.HasForeignKey("FallbackFillerId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ErsatzTV.Core.Domain.Channel", "MirrorSourceChannel")
.WithMany()
.HasForeignKey("MirrorSourceChannelId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
.WithMany()
.HasForeignKey("WatermarkId")
@ -3892,6 +3908,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -3892,6 +3908,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("FallbackFiller");
b.Navigation("MirrorSourceChannel");
b.Navigation("Watermark");
});

6
ErsatzTV.Infrastructure/Data/Configurations/ChannelConfiguration.cs

@ -43,5 +43,11 @@ public class ChannelConfiguration : IEntityTypeConfiguration<Channel> @@ -43,5 +43,11 @@ public class ChannelConfiguration : IEntityTypeConfiguration<Channel>
builder.Property(c => c.Group)
.IsRequired()
.HasDefaultValue("ErsatzTV");
builder.HasOne(i => i.MirrorSourceChannel)
.WithMany()
.HasForeignKey(i => i.MirrorSourceChannelId)
.OnDelete(DeleteBehavior.SetNull)
.IsRequired(false);
}
}

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

@ -45,6 +45,8 @@ public class ChannelRepository : IChannelRepository @@ -45,6 +45,8 @@ public class ChannelRepository : IChannelRepository
.Include(c => c.FFmpegProfile)
.Include(c => c.Artwork)
.Include(c => c.Playouts)
.Include(c => c.MirrorSourceChannel)
.ThenInclude(mc => mc.Playouts)
.ToListAsync(cancellationToken);
}

41
ErsatzTV/Pages/ChannelEditor.razor

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
@page "/channels/{Id:int?}"
@page "/channels/add"
@using System.Globalization
@using ErsatzTV.Application.Artworks
@using ErsatzTV.Application.Channels
@using ErsatzTV.Application.FFmpegProfiles
@ -69,13 +70,41 @@ @@ -69,13 +70,41 @@
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Playout Mode</MudText>
<MudText>Playout Source</MudText>
</div>
<MudSelect @bind-Value="_model.PlayoutMode" For="@(() => _model.PlayoutMode)" HelperText="Controls the progression of the channel's playout">
<MudSelectItem Value="@(ChannelPlayoutMode.Continuous)">Continuous</MudSelectItem>
<MudSelectItem Value="@(ChannelPlayoutMode.OnDemand)">On Demand</MudSelectItem>
<MudSelect @bind-Value="_model.PlayoutSource" For="@(() => _model.PlayoutSource)" HelperText="Controls the source of the channel's content">
<MudSelectItem Value="@(ChannelPlayoutSource.Generated)">Generated</MudSelectItem>
<MudSelectItem Value="@(ChannelPlayoutSource.Mirror)">Mirror</MudSelectItem>
</MudSelect>
</MudStack>
@if (_model.PlayoutSource is ChannelPlayoutSource.Mirror)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Mirror Source Channel</MudText>
</div>
<MudSelect @bind-Value="_model.MirrorSourceChannelId" For="@(() => _model.MirrorSourceChannelId)"
Clearable="true">
<MudSelectItem T="int?" Value="@((int?)null)">(none)</MudSelectItem>
@foreach (ChannelViewModel channel in _channels.OrderBy(c => decimal.Parse(c.Number, CultureInfo.InvariantCulture)))
{
<MudSelectItem T="int?" Value="@channel.Id">(@channel.Number) - @channel.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
}
else
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Playout Mode</MudText>
</div>
<MudSelect @bind-Value="_model.PlayoutMode" For="@(() => _model.PlayoutMode)" HelperText="Controls the progression of the channel's playout">
<MudSelectItem Value="@(ChannelPlayoutMode.Continuous)">Continuous</MudSelectItem>
<MudSelectItem Value="@(ChannelPlayoutMode.OnDemand)">On Demand</MudSelectItem>
</MudSelect>
</MudStack>
}
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Is Enabled</MudText>
@ -285,6 +314,7 @@ else @@ -285,6 +314,7 @@ else
private readonly ChannelEditViewModelValidator _validator = new();
private MudForm _form;
private List<ChannelViewModel> _channels = [];
private List<FFmpegProfileViewModel> _ffmpegProfiles = [];
private List<LanguageCodeViewModel> _availableCultures = [];
private List<WatermarkViewModel> _watermarks = [];
@ -309,6 +339,7 @@ else @@ -309,6 +339,7 @@ else
{
var loadingTasks = new List<Task>
{
LoadDataAsync(() => Mediator.Send(new GetAllChannels(), token), r => _channels = r),
LoadDataAsync(() => Mediator.Send(new GetAllFFmpegProfiles(), token), r => _ffmpegProfiles = r),
LoadDataAsync(() => Mediator.Send(new GetAllLanguageCodes(), token), r => _availableCultures = r),
LoadDataAsync(() => Mediator.Send(new GetAllWatermarks(), token), r => _watermarks = r),
@ -342,7 +373,9 @@ else @@ -342,7 +373,9 @@ else
_model.Logo = channelViewModel.Logo;
}
_model.PlayoutSource = channelViewModel.PlayoutSource;
_model.PlayoutMode = channelViewModel.PlayoutMode;
_model.MirrorSourceChannelId = channelViewModel.MirrorSourceChannelId;
_model.StreamingMode = channelViewModel.StreamingMode;
_model.StreamSelectorMode = channelViewModel.StreamSelectorMode;
_model.StreamSelector = channelViewModel.StreamSelector;

6
ErsatzTV/ViewModels/ChannelEditViewModel.cs

@ -19,7 +19,9 @@ public class ChannelEditViewModel @@ -19,7 +19,9 @@ public class ChannelEditViewModel
public string PreferredAudioTitle { get; set; }
public ArtworkContentTypeModel Logo { get; set; }
public string ExternalLogoUrl { get; set; }
public ChannelPlayoutSource PlayoutSource { get; set; }
public ChannelPlayoutMode PlayoutMode { get; set; }
public int? MirrorSourceChannelId { get; set; }
public StreamingMode StreamingMode { get; set; }
public int? WatermarkId { get; set; }
public int? FallbackFillerId { get; set; }
@ -56,7 +58,9 @@ public class ChannelEditViewModel @@ -56,7 +58,9 @@ public class ChannelEditViewModel
StreamSelector,
PreferredAudioLanguageCode,
PreferredAudioTitle,
PlayoutSource,
PlayoutMode,
MirrorSourceChannelId,
StreamingMode,
WatermarkId,
FallbackFillerId,
@ -84,7 +88,9 @@ public class ChannelEditViewModel @@ -84,7 +88,9 @@ public class ChannelEditViewModel
StreamSelector,
PreferredAudioLanguageCode,
PreferredAudioTitle,
PlayoutSource,
PlayoutMode,
MirrorSourceChannelId,
StreamingMode,
WatermarkId,
FallbackFillerId,

Loading…
Cancel
Save