Browse Source

add subchannel support (#53)

pull/1213/head
Jason Dove 5 years ago committed by GitHub
parent
commit
532fdce180
  1. 2
      ErsatzTV.Application/Channels/ChannelViewModel.cs
  2. 2
      ErsatzTV.Application/Channels/Commands/CreateChannel.cs
  3. 21
      ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
  4. 2
      ErsatzTV.Application/Channels/Commands/UpdateChannel.cs
  5. 10
      ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
  6. 2
      ErsatzTV.Application/Playouts/PlayoutChannelViewModel.cs
  7. 2
      ErsatzTV.Application/Streaming/Queries/FFmpegProcessRequest.cs
  8. 2
      ErsatzTV.Application/Streaming/Queries/GetConcatPlaylistByChannelNumber.cs
  9. 2
      ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumber.cs
  10. 2
      ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumber.cs
  11. 2
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs
  12. 2
      ErsatzTV.Core/Domain/Channel.cs
  13. 2
      ErsatzTV.Core/FFmpeg/ConcatPlaylist.cs
  14. 2
      ErsatzTV.Core/Hdhr/LineupItem.cs
  15. 2
      ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs
  16. 4
      ErsatzTV.Core/Iptv/ChannelGuide.cs
  17. 2
      ErsatzTV.Infrastructure/Data/DbInitializer.cs
  18. 2
      ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs
  19. 1497
      ErsatzTV.Infrastructure/Migrations/20210309022754_Update_ChannelNumberType.Designer.cs
  20. 27
      ErsatzTV.Infrastructure/Migrations/20210309022754_Update_ChannelNumberType.cs
  21. 4
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  22. 4
      ErsatzTV/Controllers/InternalController.cs
  23. 4
      ErsatzTV/Controllers/IptvController.cs
  24. 9
      ErsatzTV/Pages/ChannelEditor.razor
  25. 4
      ErsatzTV/Validators/ChannelEditViewModelValidator.cs
  26. 2
      ErsatzTV/ViewModels/ChannelEditViewModel.cs

2
ErsatzTV.Application/Channels/ChannelViewModel.cs

@ -4,7 +4,7 @@ namespace ErsatzTV.Application.Channels
{ {
public record ChannelViewModel( public record ChannelViewModel(
int Id, int Id,
int Number, string Number,
string Name, string Name,
int FFmpegProfileId, int FFmpegProfileId,
string Logo, string Logo,

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

@ -8,7 +8,7 @@ namespace ErsatzTV.Application.Channels.Commands
public record CreateChannel public record CreateChannel
( (
string Name, string Name,
int Number, string Number,
int FFmpegProfileId, int FFmpegProfileId,
string Logo, string Logo,
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>; StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;

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

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
@ -35,7 +36,7 @@ namespace ErsatzTV.Application.Channels.Commands
_channelRepository.Add(c).Map(ProjectToViewModel); _channelRepository.Add(c).Map(ProjectToViewModel);
private async Task<Validation<BaseError, Channel>> Validate(CreateChannel request) => private async Task<Validation<BaseError, Channel>> Validate(CreateChannel request) =>
(ValidateName(request), ValidateNumber(request), await FFmpegProfileMustExist(request)) (ValidateName(request), await ValidateNumber(request), await FFmpegProfileMustExist(request))
.Apply( .Apply(
(name, number, ffmpegProfileId) => (name, number, ffmpegProfileId) =>
{ {
@ -66,9 +67,21 @@ namespace ErsatzTV.Application.Channels.Commands
createChannel.NotEmpty(c => c.Name) createChannel.NotEmpty(c => c.Name)
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name)); .Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
// TODO: validate number does not exist? private async Task<Validation<BaseError, string>> ValidateNumber(CreateChannel createChannel)
private Validation<BaseError, int> ValidateNumber(CreateChannel createChannel) => {
createChannel.AtLeast(1)(c => c.Number); Option<Channel> maybeExistingChannel = await _channelRepository.GetByNumber(createChannel.Number);
return maybeExistingChannel.Match<Validation<BaseError, string>>(
_ => BaseError.New("Channel number must be unique"),
() =>
{
if (Regex.IsMatch(createChannel.Number, @"^[0-9]+(\.[0-9])?$"))
{
return createChannel.Number;
}
return BaseError.New("Invalid channel number; one decimal is allowed for subchannels");
});
}
private async Task<Validation<BaseError, int>> FFmpegProfileMustExist(CreateChannel createChannel) => private async Task<Validation<BaseError, int>> FFmpegProfileMustExist(CreateChannel createChannel) =>
(await _ffmpegProfileRepository.Get(createChannel.FFmpegProfileId)) (await _ffmpegProfileRepository.Get(createChannel.FFmpegProfileId))

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

@ -9,7 +9,7 @@ namespace ErsatzTV.Application.Channels.Commands
( (
int ChannelId, int ChannelId,
string Name, string Name,
int Number, string Number,
int FFmpegProfileId, int FFmpegProfileId,
string Logo, string Logo,
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>; StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;

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

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
@ -75,13 +76,18 @@ namespace ErsatzTV.Application.Channels.Commands
updateChannel.NotEmpty(c => c.Name) updateChannel.NotEmpty(c => c.Name)
.Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name)); .Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name));
private async Task<Validation<BaseError, int>> ValidateNumber(UpdateChannel updateChannel) private async Task<Validation<BaseError, string>> ValidateNumber(UpdateChannel updateChannel)
{ {
Option<Channel> match = await _channelRepository.GetByNumber(updateChannel.Number); Option<Channel> match = await _channelRepository.GetByNumber(updateChannel.Number);
int matchId = match.Map(c => c.Id).IfNone(updateChannel.ChannelId); int matchId = match.Map(c => c.Id).IfNone(updateChannel.ChannelId);
if (matchId == updateChannel.ChannelId) if (matchId == updateChannel.ChannelId)
{ {
return updateChannel.AtLeast(1)(c => c.Number); if (Regex.IsMatch(updateChannel.Number, @"^[0-9](\.[0-9])?$"))
{
return updateChannel.Number;
}
return BaseError.New("Invalid channel number; one decimal is allowed for subchannels");
} }
return BaseError.New("Channel number must be unique"); return BaseError.New("Channel number must be unique");

2
ErsatzTV.Application/Playouts/PlayoutChannelViewModel.cs

@ -1,4 +1,4 @@
namespace ErsatzTV.Application.Playouts namespace ErsatzTV.Application.Playouts
{ {
public record PlayoutChannelViewModel(int Id, int Number, string Name); public record PlayoutChannelViewModel(int Id, string Number, string Name);
} }

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

@ -5,5 +5,5 @@ using MediatR;
namespace ErsatzTV.Application.Streaming.Queries namespace ErsatzTV.Application.Streaming.Queries
{ {
public record FFmpegProcessRequest(int ChannelNumber) : IRequest<Either<BaseError, Process>>; public record FFmpegProcessRequest(string ChannelNumber) : IRequest<Either<BaseError, Process>>;
} }

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

@ -6,5 +6,5 @@ using MediatR;
namespace ErsatzTV.Application.Streaming.Queries namespace ErsatzTV.Application.Streaming.Queries
{ {
public record GetConcatPlaylistByChannelNumber public record GetConcatPlaylistByChannelNumber
(string Scheme, string Host, int ChannelNumber) : IRequest<Either<BaseError, ConcatPlaylist>>; (string Scheme, string Host, string ChannelNumber) : IRequest<Either<BaseError, ConcatPlaylist>>;
} }

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

@ -2,7 +2,7 @@
{ {
public record GetConcatProcessByChannelNumber : FFmpegProcessRequest public record GetConcatProcessByChannelNumber : FFmpegProcessRequest
{ {
public GetConcatProcessByChannelNumber(string scheme, string host, int channelNumber) : base(channelNumber) public GetConcatProcessByChannelNumber(string scheme, string host, string channelNumber) : base(channelNumber)
{ {
Scheme = scheme; Scheme = scheme;
Host = host; Host = host;

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

@ -5,5 +5,5 @@ using MediatR;
namespace ErsatzTV.Application.Streaming.Queries namespace ErsatzTV.Application.Streaming.Queries
{ {
public record GetHlsPlaylistByChannelNumber public record GetHlsPlaylistByChannelNumber
(string Scheme, string Host, int ChannelNumber) : IRequest<Either<BaseError, string>>; (string Scheme, string Host, string ChannelNumber) : IRequest<Either<BaseError, string>>;
} }

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

@ -2,7 +2,7 @@
{ {
public record GetPlayoutItemProcessByChannelNumber : FFmpegProcessRequest public record GetPlayoutItemProcessByChannelNumber : FFmpegProcessRequest
{ {
public GetPlayoutItemProcessByChannelNumber(int channelNumber) : base(channelNumber) public GetPlayoutItemProcessByChannelNumber(string channelNumber) : base(channelNumber)
{ {
} }
} }

2
ErsatzTV.Core/Domain/Channel.cs

@ -8,7 +8,7 @@ namespace ErsatzTV.Core.Domain
public Channel(Guid uniqueId) => UniqueId = uniqueId; public Channel(Guid uniqueId) => UniqueId = uniqueId;
public int Id { get; set; } public int Id { get; set; }
public Guid UniqueId { get; init; } public Guid UniqueId { get; init; }
public int Number { get; set; } public string Number { get; set; }
public string Name { get; set; } public string Name { get; set; }
public int FFmpegProfileId { get; set; } public int FFmpegProfileId { get; set; }
public FFmpegProfile FFmpegProfile { get; set; } public FFmpegProfile FFmpegProfile { get; set; }

2
ErsatzTV.Core/FFmpeg/ConcatPlaylist.cs

@ -1,6 +1,6 @@
namespace ErsatzTV.Core.FFmpeg namespace ErsatzTV.Core.FFmpeg
{ {
public record ConcatPlaylist(string Scheme, string Host, int ChannelNumber) public record ConcatPlaylist(string Scheme, string Host, string ChannelNumber)
{ {
public override string ToString() => public override string ToString() =>
$@"ffconcat version 1.0 $@"ffconcat version 1.0

2
ErsatzTV.Core/Hdhr/LineupItem.cs

@ -17,7 +17,7 @@ namespace ErsatzTV.Core.Hdhr
_channel = channel; _channel = channel;
} }
public string GuideNumber => _channel.Number.ToString(); public string GuideNumber => _channel.Number;
public string GuideName => _channel.Name; public string GuideName => _channel.Name;
public string URL => _channel.StreamingMode switch public string URL => _channel.StreamingMode switch

2
ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs

@ -9,7 +9,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
{ {
Task<Channel> Add(Channel channel); Task<Channel> Add(Channel channel);
Task<Option<Channel>> Get(int id); Task<Option<Channel>> Get(int id);
Task<Option<Channel>> GetByNumber(int number); Task<Option<Channel>> GetByNumber(string number);
Task<List<Channel>> GetAll(); Task<List<Channel>> GetAll();
Task<List<Channel>> GetAllForGuide(); Task<List<Channel>> GetAllForGuide();
Task Update(Channel channel); Task Update(Channel channel);

4
ErsatzTV.Core/Iptv/ChannelGuide.cs

@ -33,7 +33,7 @@ namespace ErsatzTV.Core.Iptv
foreach (Channel channel in _channels) foreach (Channel channel in _channels)
{ {
xml.WriteStartElement("channel"); xml.WriteStartElement("channel");
xml.WriteAttributeString("id", channel.Number.ToString()); xml.WriteAttributeString("id", channel.Number);
xml.WriteStartElement("display-name"); xml.WriteStartElement("display-name");
xml.WriteAttributeString("lang", "en"); xml.WriteAttributeString("lang", "en");
@ -87,7 +87,7 @@ namespace ErsatzTV.Core.Iptv
xml.WriteStartElement("programme"); xml.WriteStartElement("programme");
xml.WriteAttributeString("start", start); xml.WriteAttributeString("start", start);
xml.WriteAttributeString("stop", stop); xml.WriteAttributeString("stop", stop);
xml.WriteAttributeString("channel", channel.Number.ToString()); xml.WriteAttributeString("channel", channel.Number);
xml.WriteStartElement("title"); xml.WriteStartElement("title");
xml.WriteAttributeString("lang", "en"); xml.WriteAttributeString("lang", "en");

2
ErsatzTV.Infrastructure/Data/DbInitializer.cs

@ -47,7 +47,7 @@ namespace ErsatzTV.Infrastructure.Data
var defaultChannel = new Channel(Guid.NewGuid()) var defaultChannel = new Channel(Guid.NewGuid())
{ {
Number = 1, Number = "1",
Name = "ErsatzTV", Name = "ErsatzTV",
FFmpegProfile = defaultProfile, FFmpegProfile = defaultProfile,
StreamingMode = StreamingMode.TransportStream StreamingMode = StreamingMode.TransportStream

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

@ -29,7 +29,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.SingleOrDefaultAsync(c => c.Id == id) .SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional); .Map(Optional);
public Task<Option<Channel>> GetByNumber(int number) => public Task<Option<Channel>> GetByNumber(string number) =>
_dbContext.Channels _dbContext.Channels
.Include(c => c.FFmpegProfile) .Include(c => c.FFmpegProfile)
.ThenInclude(p => p.Resolution) .ThenInclude(p => p.Resolution)

1497
ErsatzTV.Infrastructure/Migrations/20210309022754_Update_ChannelNumberType.Designer.cs generated

File diff suppressed because it is too large Load Diff

27
ErsatzTV.Infrastructure/Migrations/20210309022754_Update_ChannelNumberType.cs

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Update_ChannelNumberType : Migration
{
protected override void Up(MigrationBuilder migrationBuilder) =>
migrationBuilder.AlterColumn<string>(
"Number",
"Channel",
"TEXT",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
protected override void Down(MigrationBuilder migrationBuilder) =>
migrationBuilder.AlterColumn<int>(
"Number",
"Channel",
"INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
}
}

4
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -80,8 +80,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("Number") b.Property<string>("Number")
.HasColumnType("INTEGER"); .HasColumnType("TEXT");
b.Property<int>("StreamingMode") b.Property<int>("StreamingMode")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

4
ErsatzTV/Controllers/InternalController.cs

@ -22,12 +22,12 @@ namespace ErsatzTV.Controllers
} }
[HttpGet("ffmpeg/concat/{channelNumber}")] [HttpGet("ffmpeg/concat/{channelNumber}")]
public Task<IActionResult> GetConcatPlaylist(int channelNumber) => public Task<IActionResult> GetConcatPlaylist(string channelNumber) =>
_mediator.Send(new GetConcatPlaylistByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber)) _mediator.Send(new GetConcatPlaylistByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber))
.ToActionResult(); .ToActionResult();
[HttpGet("ffmpeg/stream/{channelNumber}")] [HttpGet("ffmpeg/stream/{channelNumber}")]
public Task<IActionResult> GetStream(int channelNumber) => public Task<IActionResult> GetStream(string channelNumber) =>
_mediator.Send(new GetPlayoutItemProcessByChannelNumber(channelNumber)).Map( _mediator.Send(new GetPlayoutItemProcessByChannelNumber(channelNumber)).Map(
result => result =>
result.Match<IActionResult>( result.Match<IActionResult>(

4
ErsatzTV/Controllers/IptvController.cs

@ -37,7 +37,7 @@ namespace ErsatzTV.Controllers
.Map<ChannelGuide, IActionResult>(Ok); .Map<ChannelGuide, IActionResult>(Ok);
[HttpGet("iptv/channel/{channelNumber}.ts")] [HttpGet("iptv/channel/{channelNumber}.ts")]
public Task<IActionResult> GetTransportStreamVideo(int channelNumber) => public Task<IActionResult> GetTransportStreamVideo(string channelNumber) =>
_mediator.Send(new GetConcatProcessByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber)) _mediator.Send(new GetConcatProcessByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber))
.Map( .Map(
result => result.Match<IActionResult>( result => result.Match<IActionResult>(
@ -50,7 +50,7 @@ namespace ErsatzTV.Controllers
error => BadRequest(error.Value))); error => BadRequest(error.Value)));
[HttpGet("iptv/channel/{channelNumber}.m3u8")] [HttpGet("iptv/channel/{channelNumber}.m3u8")]
public Task<IActionResult> GetHttpLiveStreamingVideo(int channelNumber) => public Task<IActionResult> GetHttpLiveStreamingVideo(string channelNumber) =>
_mediator.Send(new GetHlsPlaylistByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber)) _mediator.Send(new GetHlsPlaylistByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber))
.Map( .Map(
result => result.Match<IActionResult>( result => result.Match<IActionResult>(

9
ErsatzTV/Pages/ChannelEditor.razor

@ -93,8 +93,9 @@
else else
{ {
// TODO: command for new channel // TODO: command for new channel
int maxNumber = await Mediator.Send(new GetAllChannels()).Map(channels => channels.Max(c => c.Number)); int maxNumber = await Mediator.Send(new GetAllChannels())
_model.Number = maxNumber + 1; .Map(list => list.Map(c => int.TryParse(c.Number.Split(".").Head(), out int result) ? result : 0).Max());
_model.Number = (maxNumber + 1).ToString();
_model.Name = "New Channel"; _model.Name = "New Channel";
_model.FFmpegProfileId = _ffmpegProfiles.Head().Id; _model.FFmpegProfileId = _ffmpegProfiles.Head().Id;
_model.StreamingMode = StreamingMode.TransportStream; _model.StreamingMode = StreamingMode.TransportStream;
@ -124,7 +125,7 @@
errorMessage.HeadOrNone().Match( errorMessage.HeadOrNone().Match(
error => error =>
{ {
Snackbar.Add($"Unexpected error saving channel: {error.Value}"); Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Unexpected error saving channel: {Error}", error.Value); Logger.LogError("Unexpected error saving channel: {Error}", error.Value);
}, },
() => NavigationManager.NavigateTo("/channels")); () => NavigationManager.NavigateTo("/channels"));
@ -144,7 +145,7 @@
}, },
error => error =>
{ {
Snackbar.Add($"Unexpected error saving channel logo: {error.Value}"); Snackbar.Add($"Unexpected error saving channel logo: {error.Value}", Severity.Error);
Logger.LogError("Unexpected error saving channel logo: {Error}", error.Value); Logger.LogError("Unexpected error saving channel logo: {Error}", error.Value);
}); });
} }

4
ErsatzTV/Validators/ChannelEditViewModelValidator.cs

@ -7,7 +7,9 @@ namespace ErsatzTV.Validators
{ {
public ChannelEditViewModelValidator() public ChannelEditViewModelValidator()
{ {
RuleFor(x => x.Number).GreaterThan(0); RuleFor(x => x.Number).Matches(@"^[0-9]+(\.[0-9])?$")
.WithMessage("Invalid channel number; one decimal is allowed for subchannels");
RuleFor(x => x.Name).NotEmpty(); RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.FFmpegProfileId).GreaterThan(0); RuleFor(x => x.FFmpegProfileId).GreaterThan(0);
} }

2
ErsatzTV/ViewModels/ChannelEditViewModel.cs

@ -7,7 +7,7 @@ namespace ErsatzTV.ViewModels
{ {
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public int Number { get; set; } public string Number { get; set; }
public int FFmpegProfileId { get; set; } public int FFmpegProfileId { get; set; }
public string Logo { get; set; } public string Logo { get; set; }
public StreamingMode StreamingMode { get; set; } public StreamingMode StreamingMode { get; set; }

Loading…
Cancel
Save