Browse Source

add alternate schedule system (#1105)

* start to add program schedule alternates

* edit days of the week

* editor improvements

* save changes

* build playouts using alternate schedules

* reset playout as needed

* add priority message
pull/1106/head
Jason Dove 3 years ago committed by GitHub
parent
commit
000fc78fd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutId.cs
  2. 24
      ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutIdHandler.cs
  3. 31
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  4. 9
      ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateSchedule.cs
  5. 6
      ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItems.cs
  6. 147
      ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItemsHandler.cs
  7. 10
      ErsatzTV.Application/Playouts/Mapper.cs
  8. 9
      ErsatzTV.Application/Playouts/PlayoutAlternateScheduleViewModel.cs
  9. 3
      ErsatzTV.Application/Playouts/Queries/GetPlayoutAlternateSchedules.cs
  10. 53
      ErsatzTV.Application/Playouts/Queries/GetPlayoutAlternateSchedulesHandler.cs
  11. 54
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  12. 1
      ErsatzTV.Core/Domain/Playout.cs
  13. 1
      ErsatzTV.Core/Domain/ProgramSchedule.cs
  14. 28
      ErsatzTV.Core/Domain/ProgramScheduleAlternate.cs
  15. 60
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  16. 40
      ErsatzTV.Core/Scheduling/PlayoutScheduleSelector.cs
  17. 13
      ErsatzTV.Infrastructure/Data/Configurations/CollectionValueComparer.cs
  18. 14
      ErsatzTV.Infrastructure/Data/Configurations/EnumCollectionJsonValueConverter.cs
  19. 5
      ErsatzTV.Infrastructure/Data/Configurations/PlayoutConfiguration.cs
  20. 34
      ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleAlternateConfiguration.cs
  21. 5
      ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleConfiguration.cs
  22. 1
      ErsatzTV.Infrastructure/Data/TvContext.cs
  23. 4394
      ErsatzTV.Infrastructure/Migrations/20230108192830_Add_ProgramScheduleAlternates.Designer.cs
  24. 61
      ErsatzTV.Infrastructure/Migrations/20230108192830_Add_ProgramScheduleAlternates.cs
  25. 544
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  26. 486
      ErsatzTV/Pages/PlayoutAlternateSchedulesEditor.razor
  27. 7
      ErsatzTV/Pages/Playouts.razor
  28. 12
      ErsatzTV/Validators/PlayoutAlternateScheduleEditViewModelValidator.cs
  29. 13
      ErsatzTV/ViewModels/PlayoutAlternateScheduleEditViewModel.cs

3
ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutId.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelNameByPlayoutId(int PlayoutId) : IRequest<Option<string>>;

24
ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutIdHandler.cs

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Channels;
public class GetChannelNameByPlayoutIdHandler : IRequestHandler<GetChannelNameByPlayoutId, Option<string>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetChannelNameByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public async Task<Option<string>> Handle(GetChannelNameByPlayoutId request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Playouts
.Include(p => p.Channel)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId)
.MapT(p => p.Channel.Name);
}
}

31
ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs

@ -75,6 +75,36 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -75,6 +75,36 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
dbContext.Playouts
.Include(p => p.Channel)
.Include(p => p.Items)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.Collection)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MediaItem)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.PreRollFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MidRollFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.PostRollFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.TailFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.MediaItem)
.Include(p => p.ProgramSchedule)
@ -98,6 +128,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -98,6 +128,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller)
.SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId)
.Map(o => o.ToValidation<BaseError>("Playout does not exist."));
}

9
ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateSchedule.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
namespace ErsatzTV.Application.Playouts;
public record ReplacePlayoutAlternateSchedule(
int Id,
int Index,
int ProgramScheduleId,
List<DayOfWeek> DaysOfWeek,
List<int> DaysOfMonth,
List<int> MonthsOfYear);

6
ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItems.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Playouts;
public record ReplacePlayoutAlternateScheduleItems
(int PlayoutId, List<ReplacePlayoutAlternateSchedule> Items) : IRequest<Either<BaseError, Unit>>;

147
ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItemsHandler.cs

@ -0,0 +1,147 @@ @@ -0,0 +1,147 @@
using System.Threading.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Playouts;
public class ReplacePlayoutAlternateScheduleItemsHandler :
IRequestHandler<ReplacePlayoutAlternateScheduleItems, Either<BaseError, Unit>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly ILogger<ReplacePlayoutAlternateScheduleItemsHandler> _logger;
public ReplacePlayoutAlternateScheduleItemsHandler(
IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> channel,
ILogger<ReplacePlayoutAlternateScheduleItemsHandler> logger)
{
_dbContextFactory = dbContextFactory;
_channel = channel;
_logger = logger;
}
public async Task<Either<BaseError, Unit>> Handle(
ReplacePlayoutAlternateScheduleItems request,
CancellationToken cancellationToken)
{
// TODO: validate that items is not empty
try
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Playout> maybePlayout = await dbContext.Playouts
.Include(p => p.ProgramSchedule)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(p => p.ProgramSchedule)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId);
foreach (Playout playout in maybePlayout)
{
ProgramSchedule existingDefault = playout.ProgramSchedule;
// exclude highest index
int maxIndex = request.Items.Map(x => x.Index).Max();
ReplacePlayoutAlternateSchedule highest = request.Items.First(x => x.Index == maxIndex);
ProgramScheduleAlternate[] existing = playout.ProgramScheduleAlternates.ToArray();
var incoming = request.Items.Except(new[] { highest }).ToList();
var toAdd = incoming.Filter(x => existing.All(e => e.Id != x.Id)).ToList();
var toRemove = existing.Filter(e => incoming.All(m => m.Id != e.Id)).ToList();
var toUpdate = incoming.Except(toAdd).ToList();
playout.ProgramScheduleAlternates.RemoveAll(toRemove.Contains);
foreach (ReplacePlayoutAlternateSchedule add in toAdd)
{
playout.ProgramScheduleAlternates.Add(
new ProgramScheduleAlternate
{
PlayoutId = playout.Id,
Index = add.Index,
ProgramScheduleId = add.ProgramScheduleId,
DaysOfWeek = add.DaysOfWeek,
DaysOfMonth = add.DaysOfMonth,
MonthsOfYear = add.MonthsOfYear
});
}
foreach (ReplacePlayoutAlternateSchedule update in toUpdate)
{
foreach (ProgramScheduleAlternate ex in existing.Filter(x => x.Id == update.Id))
{
ex.Index = update.Index;
ex.ProgramScheduleId = update.ProgramScheduleId;
ex.DaysOfWeek = update.DaysOfWeek;
ex.DaysOfMonth = update.DaysOfMonth;
ex.MonthsOfYear = update.MonthsOfYear;
}
}
// save highest index directly to playout
if (playout.ProgramScheduleId != highest.ProgramScheduleId)
{
playout.ProgramScheduleId = highest.ProgramScheduleId;
}
await dbContext.SaveChangesAsync(cancellationToken);
Option<PlayoutItem> maybePlayoutItem = await dbContext.PlayoutItems
.Filter(pi => pi.PlayoutId == request.PlayoutId)
.OrderByDescending(pi => pi.Start)
.FirstOrDefaultAsync(cancellationToken)
.Map(Optional);
foreach (PlayoutItem playoutItem in maybePlayoutItem)
{
DateTimeOffset start = DateTimeOffset.Now;
var daysToCheck = Enumerable.Range(0, (playoutItem.StartOffset - start).Days + 1)
.Select(d => start.AddDays(d))
.ToList();
foreach (DateTimeOffset dayToCheck in daysToCheck)
{
ProgramSchedule oldSchedule = PlayoutScheduleSelector.GetProgramScheduleFor(
existingDefault,
existing,
dayToCheck);
ProgramSchedule newSchedule = PlayoutScheduleSelector.GetProgramScheduleFor(
playout.ProgramSchedule,
playout.ProgramScheduleAlternates,
dayToCheck);
if (oldSchedule.Id != newSchedule.Id)
{
_logger.LogInformation(
"Alternate schedule change detected for day {Day}, schedule {One} => {Two}; will refresh playout",
dayToCheck,
oldSchedule.Name,
newSchedule.Name);
await _channel.WriteAsync(
new BuildPlayout(request.PlayoutId, PlayoutBuildMode.Refresh),
cancellationToken);
break;
}
}
}
}
return Unit.Default;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
}

10
ErsatzTV.Application/Playouts/Mapper.cs

@ -10,6 +10,16 @@ internal static class Mapper @@ -10,6 +10,16 @@ internal static class Mapper
playoutItem.StartOffset,
GetDisplayDuration(playoutItem.FinishOffset - playoutItem.StartOffset));
internal static PlayoutAlternateScheduleViewModel ProjectToViewModel(
ProgramScheduleAlternate programScheduleAlternate) =>
new(
programScheduleAlternate.Id,
programScheduleAlternate.Index,
programScheduleAlternate.ProgramScheduleId,
programScheduleAlternate.DaysOfWeek,
programScheduleAlternate.DaysOfMonth,
programScheduleAlternate.MonthsOfYear);
private static string GetDisplayTitle(PlayoutItem playoutItem)
{
switch (playoutItem.MediaItem)

9
ErsatzTV.Application/Playouts/PlayoutAlternateScheduleViewModel.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
namespace ErsatzTV.Application.Playouts;
public record PlayoutAlternateScheduleViewModel(
int Id,
int Index,
int ProgramScheduleId,
ICollection<DayOfWeek> DaysOfWeek,
ICollection<int> DaysOfMonth,
ICollection<int> MonthsOfYear);

3
ErsatzTV.Application/Playouts/Queries/GetPlayoutAlternateSchedules.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Playouts;
public record GetPlayoutAlternateSchedules(int PlayoutId) : IRequest<List<PlayoutAlternateScheduleViewModel>>;

53
ErsatzTV.Application/Playouts/Queries/GetPlayoutAlternateSchedulesHandler.cs

@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Playouts.Mapper;
namespace ErsatzTV.Application.Playouts;
public class GetPlayoutAlternateSchedulesHandler :
IRequestHandler<GetPlayoutAlternateSchedules, List<PlayoutAlternateScheduleViewModel>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetPlayoutAlternateSchedulesHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<PlayoutAlternateScheduleViewModel>> Handle(
GetPlayoutAlternateSchedules request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<PlayoutAlternateScheduleViewModel> result = await dbContext.ProgramScheduleAlternates
.Filter(psa => psa.PlayoutId == request.PlayoutId)
.Include(psa => psa.ProgramSchedule)
.Map(psa => ProjectToViewModel(psa))
.ToListAsync(cancellationToken);
Option<ProgramSchedule> maybeDefaultSchedule = await dbContext.Playouts
.Include(p => p.ProgramSchedule)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId)
.MapT(p => p.ProgramSchedule);
foreach (ProgramSchedule defaultSchedule in maybeDefaultSchedule)
{
var psa = new ProgramScheduleAlternate
{
Id = -1,
PlayoutId = request.PlayoutId,
ProgramScheduleId = defaultSchedule.Id,
ProgramSchedule = defaultSchedule,
Index = result.Map(i => i.Index).DefaultIfEmpty().Max() + 1,
DaysOfMonth = ProgramScheduleAlternate.AllDaysOfMonth(),
DaysOfWeek = ProgramScheduleAlternate.AllDaysOfWeek(),
MonthsOfYear = ProgramScheduleAlternate.AllMonthsOfYear()
};
result.Add(ProjectToViewModel(psa));
}
return result;
}
}

54
ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs

@ -543,7 +543,8 @@ public class PlayoutBuilderTests @@ -543,7 +543,8 @@ public class PlayoutBuilderTests
},
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -639,7 +640,8 @@ public class PlayoutBuilderTests @@ -639,7 +640,8 @@ public class PlayoutBuilderTests
},
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -783,7 +785,8 @@ public class PlayoutBuilderTests @@ -783,7 +785,8 @@ public class PlayoutBuilderTests
},
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -885,7 +888,8 @@ public class PlayoutBuilderTests @@ -885,7 +888,8 @@ public class PlayoutBuilderTests
},
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -996,7 +1000,8 @@ public class PlayoutBuilderTests @@ -996,7 +1000,8 @@ public class PlayoutBuilderTests
InFlood = true
},
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -1100,7 +1105,8 @@ public class PlayoutBuilderTests @@ -1100,7 +1105,8 @@ public class PlayoutBuilderTests
},
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -1208,7 +1214,8 @@ public class PlayoutBuilderTests @@ -1208,7 +1214,8 @@ public class PlayoutBuilderTests
},
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -1321,7 +1328,8 @@ public class PlayoutBuilderTests @@ -1321,7 +1328,8 @@ public class PlayoutBuilderTests
MultipleRemaining = 2
},
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -1423,7 +1431,8 @@ public class PlayoutBuilderTests @@ -1423,7 +1431,8 @@ public class PlayoutBuilderTests
},
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -1536,7 +1545,8 @@ public class PlayoutBuilderTests @@ -1536,7 +1545,8 @@ public class PlayoutBuilderTests
DurationFinish = HoursAfterMidnight(3).UtcDateTime
},
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -1660,7 +1670,8 @@ public class PlayoutBuilderTests @@ -1660,7 +1670,8 @@ public class PlayoutBuilderTests
},
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -1776,7 +1787,8 @@ public class PlayoutBuilderTests @@ -1776,7 +1787,8 @@ public class PlayoutBuilderTests
},
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -1852,7 +1864,8 @@ public class PlayoutBuilderTests @@ -1852,7 +1864,8 @@ public class PlayoutBuilderTests
},
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -2049,7 +2062,8 @@ public class PlayoutBuilderTests @@ -2049,7 +2062,8 @@ public class PlayoutBuilderTests
},
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
playout.ProgramScheduleAnchors.Add(
@ -2437,7 +2451,8 @@ public class PlayoutBuilderTests @@ -2437,7 +2451,8 @@ public class PlayoutBuilderTests
InFlood = true
},
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -2548,7 +2563,8 @@ public class PlayoutBuilderTests @@ -2548,7 +2563,8 @@ public class PlayoutBuilderTests
MultipleRemaining = 2
},
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -2659,7 +2675,8 @@ public class PlayoutBuilderTests @@ -2659,7 +2675,8 @@ public class PlayoutBuilderTests
DurationFinish = HoursAfterMidnight(3).UtcDateTime
},
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
var configRepo = new Mock<IConfigElementRepository>();
@ -2768,7 +2785,8 @@ public class PlayoutBuilderTests @@ -2768,7 +2785,8 @@ public class PlayoutBuilderTests
ProgramSchedule = new ProgramSchedule { Items = items },
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
Items = new List<PlayoutItem>(),
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>()
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
return new TestData(builder, playout);

1
ErsatzTV.Core/Domain/Playout.cs

@ -7,6 +7,7 @@ public class Playout @@ -7,6 +7,7 @@ public class Playout
public Channel Channel { get; set; }
public int ProgramScheduleId { get; set; }
public ProgramSchedule ProgramSchedule { get; set; }
public List<ProgramScheduleAlternate> ProgramScheduleAlternates { get; set; }
public ProgramSchedulePlayoutType ProgramSchedulePlayoutType { get; set; }
public List<PlayoutItem> Items { get; set; }
public PlayoutAnchor Anchor { get; set; }

1
ErsatzTV.Core/Domain/ProgramSchedule.cs

@ -10,4 +10,5 @@ public class ProgramSchedule @@ -10,4 +10,5 @@ public class ProgramSchedule
public bool RandomStartPoint { get; set; }
public List<ProgramScheduleItem> Items { get; set; }
public List<Playout> Playouts { get; set; }
public List<ProgramScheduleAlternate> ProgramScheduleAlternates { get; set; }
}

28
ErsatzTV.Core/Domain/ProgramScheduleAlternate.cs

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
namespace ErsatzTV.Core.Domain;
public class ProgramScheduleAlternate
{
public static List<DayOfWeek> AllDaysOfWeek() => new()
{
DayOfWeek.Monday,
DayOfWeek.Tuesday,
DayOfWeek.Wednesday,
DayOfWeek.Thursday,
DayOfWeek.Friday,
DayOfWeek.Saturday,
DayOfWeek.Sunday
};
public static List<int> AllDaysOfMonth() => Enumerable.Range(1, 31).ToList();
public static List<int> AllMonthsOfYear() => Enumerable.Range(1, 12).ToList();
public int Id { get; set; }
public int PlayoutId { get; set; }
public Playout Playout { get; set; }
public int ProgramScheduleId { get; set; }
public ProgramSchedule ProgramSchedule { get; set; }
public int Index { get; set; }
public ICollection<DayOfWeek> DaysOfWeek { get; set; }
public ICollection<int> DaysOfMonth { get; set; }
public ICollection<int> MonthsOfYear { get; set; }
}

60
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -203,7 +203,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -203,7 +203,7 @@ public class PlayoutBuilder : IPlayoutBuilder
parameters.Start,
parameters.Finish,
parameters.CollectionMediaItems,
playout.ProgramSchedule.RandomStartPoint);
true);
return playout;
}
@ -237,11 +237,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -237,11 +237,7 @@ public class PlayoutBuilder : IPlayoutBuilder
Map<CollectionKey, List<MediaItem>> collectionMediaItems = await GetCollectionMediaItems(playout);
if (!collectionMediaItems.Any())
{
_logger.LogWarning(
"Playout {Playout} schedule {Schedule} has no items",
playout.Channel.Name,
playout.ProgramSchedule.Name);
_logger.LogWarning("Playout {Playout} has no items", playout.Channel.Name);
return None;
}
@ -365,13 +361,21 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -365,13 +361,21 @@ public class PlayoutBuilder : IPlayoutBuilder
bool saveAnchorDate,
bool randomStartPoint)
{
var sortedScheduleItems = playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList();
ProgramSchedule activeSchedule = PlayoutScheduleSelector.GetProgramScheduleFor(
playout.ProgramSchedule,
playout.ProgramScheduleAlternates,
playoutStart);
// random start points are disabled in some scenarios, so ensure it's enabled and active
randomStartPoint = randomStartPoint && activeSchedule.RandomStartPoint;
var sortedScheduleItems = activeSchedule.Items.OrderBy(i => i.Index).ToList();
CollectionEnumeratorState scheduleItemsEnumeratorState =
playout.Anchor?.ScheduleItemsEnumeratorState ?? new CollectionEnumeratorState
{ Seed = Random.Next(), Index = 0 };
IScheduleItemsEnumerator scheduleItemsEnumerator = playout.ProgramSchedule.ShuffleScheduleItems
? new ShuffledScheduleItemsEnumerator(playout.ProgramSchedule.Items, scheduleItemsEnumeratorState)
: new OrderedScheduleItemsEnumerator(playout.ProgramSchedule.Items, scheduleItemsEnumeratorState);
IScheduleItemsEnumerator scheduleItemsEnumerator = activeSchedule.ShuffleScheduleItems
? new ShuffledScheduleItemsEnumerator(activeSchedule.Items, scheduleItemsEnumeratorState)
: new OrderedScheduleItemsEnumerator(activeSchedule.Items, scheduleItemsEnumeratorState);
var collectionEnumerators = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in collectionMediaItems)
{
@ -381,7 +385,13 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -381,7 +385,13 @@ public class PlayoutBuilder : IPlayoutBuilder
PlaybackOrder playbackOrder = maybeScheduleItem
.Match(item => item.PlaybackOrder, () => PlaybackOrder.Shuffle);
IMediaCollectionEnumerator enumerator =
await GetMediaCollectionEnumerator(playout, collectionKey, mediaItems, playbackOrder, randomStartPoint);
await GetMediaCollectionEnumerator(
playout,
activeSchedule,
collectionKey,
mediaItems,
playbackOrder,
randomStartPoint);
collectionEnumerators.Add(collectionKey, enumerator);
}
@ -533,7 +543,11 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -533,7 +543,11 @@ public class PlayoutBuilder : IPlayoutBuilder
}
// build program schedule anchors
playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators, saveAnchorDate);
playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(
playout,
activeSchedule,
collectionEnumerators,
saveAnchorDate);
return playout;
}
@ -541,6 +555,8 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -541,6 +555,8 @@ public class PlayoutBuilder : IPlayoutBuilder
private async Task<Map<CollectionKey, List<MediaItem>>> GetCollectionMediaItems(Playout playout)
{
var collectionKeys = playout.ProgramSchedule.Items
.Append(playout.ProgramScheduleAlternates.Bind(psa => psa.ProgramSchedule.Items))
.DistinctBy(i => i.Id)
.SelectMany(CollectionKeysForItem)
.Distinct()
.ToList();
@ -640,6 +656,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -640,6 +656,7 @@ public class PlayoutBuilder : IPlayoutBuilder
private static List<PlayoutProgramScheduleAnchor> BuildProgramScheduleAnchors(
Playout playout,
ProgramSchedule activeSchedule,
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
bool saveAnchorDate)
{
@ -665,8 +682,8 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -665,8 +682,8 @@ public class PlayoutBuilder : IPlayoutBuilder
{
Playout = playout,
PlayoutId = playout.Id,
ProgramSchedule = playout.ProgramSchedule,
ProgramScheduleId = playout.ProgramScheduleId,
ProgramSchedule = activeSchedule,
ProgramScheduleId = activeSchedule.Id,
CollectionType = collectionKey.CollectionType,
CollectionId = collectionKey.CollectionId,
MultiCollectionId = collectionKey.MultiCollectionId,
@ -694,6 +711,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -694,6 +711,7 @@ public class PlayoutBuilder : IPlayoutBuilder
private async Task<IMediaCollectionEnumerator> GetMediaCollectionEnumerator(
Playout playout,
ProgramSchedule activeSchedule,
CollectionKey collectionKey,
List<MediaItem> mediaItems,
PlaybackOrder playbackOrder,
@ -702,7 +720,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -702,7 +720,7 @@ public class PlayoutBuilder : IPlayoutBuilder
Option<PlayoutProgramScheduleAnchor> maybeAnchor = playout.ProgramScheduleAnchors
.OrderByDescending(a => a.AnchorDate ?? DateTime.MaxValue)
.FirstOrDefault(
a => a.ProgramScheduleId == playout.ProgramScheduleId
a => a.ProgramScheduleId == activeSchedule.Id
&& a.CollectionType == collectionKey.CollectionType
&& a.CollectionId == collectionKey.CollectionId
&& a.MultiCollectionId == collectionKey.MultiCollectionId
@ -757,7 +775,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -757,7 +775,7 @@ public class PlayoutBuilder : IPlayoutBuilder
return new ShuffleInOrderCollectionEnumerator(
await GetCollectionItemsForShuffleInOrder(collectionKey),
state,
playout.ProgramSchedule.RandomStartPoint);
activeSchedule.RandomStartPoint);
case PlaybackOrder.MultiEpisodeShuffle when
collectionKey.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow &&
collectionKey.MediaItemId.HasValue:
@ -795,7 +813,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -795,7 +813,7 @@ public class PlayoutBuilder : IPlayoutBuilder
case PlaybackOrder.MultiEpisodeShuffle:
case PlaybackOrder.Shuffle:
return new ShuffledMediaCollectionEnumerator(
await GetGroupedMediaItemsForShuffle(playout, mediaItems, collectionKey),
await GetGroupedMediaItemsForShuffle(activeSchedule, mediaItems, collectionKey),
state);
default:
// TODO: handle this error case differently?
@ -804,7 +822,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -804,7 +822,7 @@ public class PlayoutBuilder : IPlayoutBuilder
}
private async Task<List<GroupedMediaItem>> GetGroupedMediaItemsForShuffle(
Playout playout,
ProgramSchedule activeSchedule,
List<MediaItem> mediaItems,
CollectionKey collectionKey)
{
@ -816,10 +834,8 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -816,10 +834,8 @@ public class PlayoutBuilder : IPlayoutBuilder
return MultiCollectionGrouper.GroupMediaItems(collections);
}
return playout.ProgramSchedule.KeepMultiPartEpisodesTogether
? MultiPartEpisodeGrouper.GroupMediaItems(
mediaItems,
playout.ProgramSchedule.TreatCollectionsAsShows)
return activeSchedule.KeepMultiPartEpisodesTogether
? MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, activeSchedule.TreatCollectionsAsShows)
: mediaItems.Map(mi => new GroupedMediaItem(mi, null)).ToList();
}

40
ErsatzTV.Core/Scheduling/PlayoutScheduleSelector.cs

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Scheduling;
public static class PlayoutScheduleSelector
{
public static ProgramSchedule GetProgramScheduleFor(
ProgramSchedule defaultSchedule,
IEnumerable<ProgramScheduleAlternate> alternates,
DateTimeOffset date)
{
foreach (ProgramScheduleAlternate alternate in alternates.OrderBy(x => x.Index))
{
bool daysOfWeek = alternate.DaysOfWeek.Count is 0 or 7 ||
alternate.DaysOfWeek.Contains(date.DayOfWeek);
if (!daysOfWeek)
{
continue;
}
bool daysOfMonth = alternate.DaysOfMonth.Count is 0 or 31 ||
alternate.DaysOfMonth.Contains(date.Day);
if (!daysOfMonth)
{
continue;
}
bool monthOfYear = alternate.MonthsOfYear.Count is 0 or 12 ||
alternate.MonthsOfYear.Contains(date.Month);
if (monthOfYear)
{
return alternate.ProgramSchedule;
}
}
return defaultSchedule;
}
}

13
ErsatzTV.Infrastructure/Data/Configurations/CollectionValueComparer.cs

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace ErsatzTV.Infrastructure.Data.Configurations;
public class CollectionValueComparer<T> : ValueComparer<ICollection<T>>
{
public CollectionValueComparer() : base(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => c.ToHashSet())
{
}
}

14
ErsatzTV.Infrastructure/Data/Configurations/EnumCollectionJsonValueConverter.cs

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Newtonsoft.Json;
namespace ErsatzTV.Infrastructure.Data.Configurations;
public class EnumCollectionJsonValueConverter<T> : ValueConverter<ICollection<T>, string> where T : Enum
{
public EnumCollectionJsonValueConverter() : base(
v => JsonConvert.SerializeObject(v.Select(e => e.ToString()).ToList()),
v => JsonConvert.DeserializeObject<ICollection<string>>(v)
.Select(e => (T)Enum.Parse(typeof(T), e)).ToList())
{
}
}

5
ErsatzTV.Infrastructure/Data/Configurations/PlayoutConfiguration.cs

@ -10,6 +10,11 @@ public class PlayoutConfiguration : IEntityTypeConfiguration<Playout> @@ -10,6 +10,11 @@ public class PlayoutConfiguration : IEntityTypeConfiguration<Playout>
{
builder.ToTable("Playout");
builder.HasMany(p => p.ProgramScheduleAlternates)
.WithOne(a => a.Playout)
.HasForeignKey(a => a.PlayoutId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(p => p.Items)
.WithOne(pi => pi.Playout)
.HasForeignKey(pi => pi.PlayoutId)

34
ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleAlternateConfiguration.cs

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace ErsatzTV.Infrastructure.Data.Configurations;
public class ProgramScheduleAlternateConfiguration : IEntityTypeConfiguration<ProgramScheduleAlternate>
{
public void Configure(EntityTypeBuilder<ProgramScheduleAlternate> builder)
{
builder.ToTable("ProgramScheduleAlternate");
var intCollectionValueConverter = new ValueConverter<ICollection<int>, string>(
i => string.Join(",", i),
s => string.IsNullOrWhiteSpace(s)
? Array.Empty<int>()
: s.Split(new[] { ',' }).Select(int.Parse).ToArray());
var intCollectionValueComparer = new CollectionValueComparer<int>();
builder.Property(t => t.DaysOfMonth)
.HasConversion(intCollectionValueConverter)
.Metadata.SetValueComparer(intCollectionValueComparer);
builder.Property(t => t.MonthsOfYear)
.HasConversion(intCollectionValueConverter)
.Metadata.SetValueComparer(intCollectionValueComparer);
builder.Property(t => t.DaysOfWeek)
.HasConversion(new EnumCollectionJsonValueConverter<DayOfWeek>())
.Metadata.SetValueComparer(new CollectionValueComparer<DayOfWeek>());
}
}

5
ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleConfiguration.cs

@ -22,5 +22,10 @@ public class ProgramScheduleConfiguration : IEntityTypeConfiguration<ProgramSche @@ -22,5 +22,10 @@ public class ProgramScheduleConfiguration : IEntityTypeConfiguration<ProgramSche
.WithOne(p => p.ProgramSchedule)
.HasForeignKey(p => p.ProgramScheduleId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(p => p.ProgramScheduleAlternates)
.WithOne(a => a.ProgramSchedule)
.HasForeignKey(a => a.ProgramScheduleId)
.OnDelete(DeleteBehavior.Cascade);
}
}

1
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -76,6 +76,7 @@ public class TvContext : DbContext @@ -76,6 +76,7 @@ public class TvContext : DbContext
public DbSet<ProgramSchedule> ProgramSchedules { get; set; }
public DbSet<ProgramScheduleItem> ProgramScheduleItems { get; set; }
public DbSet<Playout> Playouts { get; set; }
public DbSet<ProgramScheduleAlternate> ProgramScheduleAlternates { get; set; }
public DbSet<PlayoutItem> PlayoutItems { get; set; }
public DbSet<PlayoutProgramScheduleAnchor> PlayoutProgramScheduleItemAnchors { get; set; }
public DbSet<FFmpegProfile> FFmpegProfiles { get; set; }

4394
ErsatzTV.Infrastructure/Migrations/20230108192830_Add_ProgramScheduleAlternates.Designer.cs generated

File diff suppressed because it is too large Load Diff

61
ErsatzTV.Infrastructure/Migrations/20230108192830_Add_ProgramScheduleAlternates.cs

@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddProgramScheduleAlternates : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ProgramScheduleAlternate",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
PlayoutId = table.Column<int>(type: "INTEGER", nullable: false),
ProgramScheduleId = table.Column<int>(type: "INTEGER", nullable: false),
Index = table.Column<int>(type: "INTEGER", nullable: false),
DaysOfWeek = table.Column<string>(type: "TEXT", nullable: true),
DaysOfMonth = table.Column<string>(type: "TEXT", nullable: true),
MonthsOfYear = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ProgramScheduleAlternate", x => x.Id);
table.ForeignKey(
name: "FK_ProgramScheduleAlternate_Playout_PlayoutId",
column: x => x.PlayoutId,
principalTable: "Playout",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ProgramScheduleAlternate_ProgramSchedule_ProgramScheduleId",
column: x => x.ProgramScheduleId,
principalTable: "ProgramSchedule",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleAlternate_PlayoutId",
table: "ProgramScheduleAlternate",
column: "PlayoutId");
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleAlternate_ProgramScheduleId",
table: "ProgramScheduleAlternate",
column: "ProgramScheduleId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ProgramScheduleAlternate");
}
}
}

544
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
modelBuilder.HasAnnotation("ProductVersion", "7.0.1");
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
{
@ -820,6 +820,8 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -820,6 +820,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("MediaSourceId");
b.ToTable("Library", (string)null);
b.UseTptMappingStrategy();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryFolder", b =>
@ -914,6 +916,8 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -914,6 +916,8 @@ namespace ErsatzTV.Infrastructure.Migrations
.IsUnique();
b.ToTable("MediaFile", (string)null);
b.UseTptMappingStrategy();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b =>
@ -933,6 +937,8 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -933,6 +937,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("LibraryPathId");
b.ToTable("MediaItem", (string)null);
b.UseTptMappingStrategy();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b =>
@ -944,6 +950,8 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -944,6 +950,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasKey("Id");
b.ToTable("MediaSource", (string)null);
b.UseTptMappingStrategy();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b =>
@ -1598,6 +1606,39 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1598,6 +1606,39 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("ProgramSchedule", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleAlternate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("DaysOfMonth")
.HasColumnType("TEXT");
b.Property<string>("DaysOfWeek")
.HasColumnType("TEXT");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<string>("MonthsOfYear")
.HasColumnType("TEXT");
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PlayoutId");
b.HasIndex("ProgramScheduleId");
b.ToTable("ProgramScheduleAlternate", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b =>
{
b.Property<int>("Id")
@ -1692,6 +1733,8 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1692,6 +1733,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("WatermarkId");
b.ToTable("ProgramScheduleItem", (string)null);
b.UseTptMappingStrategy();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b =>
@ -2265,13 +2308,6 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2265,13 +2308,6 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("JellyfinPathInfo");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
b.ToTable("Artist", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Library");
@ -2285,69 +2321,69 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2285,69 +2321,69 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("EmbyLibrary", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
b.HasBaseType("ErsatzTV.Core.Domain.Library");
b.Property<string>("OperatingSystem")
b.Property<string>("ItemId")
.HasColumnType("TEXT");
b.Property<string>("ServerName")
.HasColumnType("TEXT");
b.Property<bool>("ShouldSyncItems")
.HasColumnType("INTEGER");
b.ToTable("EmbyMediaSource", (string)null);
b.ToTable("JellyfinLibrary", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
b.Property<int>("SeasonId")
.HasColumnType("INTEGER");
b.HasIndex("SeasonId");
b.HasBaseType("ErsatzTV.Core.Domain.Library");
b.ToTable("Episode", (string)null);
b.ToTable("LocalLibrary", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Library");
b.Property<string>("ItemId")
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<bool>("ShouldSyncItems")
.HasColumnType("INTEGER");
b.ToTable("JellyfinLibrary", (string)null);
b.ToTable("PlexLibrary", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
b.HasBaseType("ErsatzTV.Core.Domain.MediaFile");
b.Property<string>("OperatingSystem")
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("ServerName")
.HasColumnType("TEXT");
b.Property<int>("PlexId")
.HasColumnType("INTEGER");
b.ToTable("JellyfinMediaSource", (string)null);
b.ToTable("PlexMediaFile", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Library");
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
b.ToTable("LocalLibrary", (string)null);
b.ToTable("Artist", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
b.ToTable("LocalMediaSource", (string)null);
b.Property<int>("SeasonId")
.HasColumnType("INTEGER");
b.HasIndex("SeasonId");
b.ToTable("Episode", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b =>
@ -2376,30 +2412,66 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2376,30 +2412,66 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("OtherVideo", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Library");
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<bool>("ShouldSyncItems")
b.Property<int>("ShowId")
.HasColumnType("INTEGER");
b.ToTable("PlexLibrary", (string)null);
b.HasIndex("ShowId");
b.ToTable("Season", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaFile");
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
b.Property<string>("Key")
b.ToTable("Show", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
b.ToTable("Song", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
b.Property<string>("OperatingSystem")
.HasColumnType("TEXT");
b.Property<int>("PlexId")
.HasColumnType("INTEGER");
b.Property<string>("ServerName")
.HasColumnType("TEXT");
b.ToTable("PlexMediaFile", (string)null);
b.ToTable("EmbyMediaSource", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
b.Property<string>("OperatingSystem")
.HasColumnType("TEXT");
b.Property<string>("ServerName")
.HasColumnType("TEXT");
b.ToTable("JellyfinMediaSource", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
b.ToTable("LocalMediaSource", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
@ -2461,35 +2533,6 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2461,35 +2533,6 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("ProgramScheduleOneItem", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<int>("ShowId")
.HasColumnType("INTEGER");
b.HasIndex("ShowId");
b.ToTable("Season", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
b.ToTable("Show", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
b.ToTable("Song", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyEpisode", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Episode");
@ -2503,9 +2546,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2503,9 +2546,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("EmbyEpisode", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMovie", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinEpisode", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Movie");
b.HasBaseType("ErsatzTV.Core.Domain.Episode");
b.Property<string>("Etag")
.HasColumnType("TEXT");
@ -2513,25 +2556,25 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2513,25 +2556,25 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("ItemId")
.HasColumnType("TEXT");
b.ToTable("EmbyMovie", (string)null);
b.ToTable("JellyfinEpisode", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbySeason", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Season");
b.HasBaseType("ErsatzTV.Core.Domain.Episode");
b.Property<string>("Etag")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
b.Property<string>("Key")
.HasColumnType("TEXT");
b.ToTable("EmbySeason", (string)null);
b.ToTable("PlexEpisode", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyShow", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMovie", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Show");
b.HasBaseType("ErsatzTV.Core.Domain.Movie");
b.Property<string>("Etag")
.HasColumnType("TEXT");
@ -2539,12 +2582,12 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2539,12 +2582,12 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("ItemId")
.HasColumnType("TEXT");
b.ToTable("EmbyShow", (string)null);
b.ToTable("EmbyMovie", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinEpisode", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMovie", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Episode");
b.HasBaseType("ErsatzTV.Core.Domain.Movie");
b.Property<string>("Etag")
.HasColumnType("TEXT");
@ -2552,23 +2595,23 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2552,23 +2595,23 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("ItemId")
.HasColumnType("TEXT");
b.ToTable("JellyfinEpisode", (string)null);
b.ToTable("JellyfinMovie", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMovie", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Movie");
b.Property<string>("Etag")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
b.Property<string>("Key")
.HasColumnType("TEXT");
b.ToTable("JellyfinMovie", (string)null);
b.ToTable("PlexMovie", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinSeason", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbySeason", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Season");
@ -2578,12 +2621,12 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2578,12 +2621,12 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("ItemId")
.HasColumnType("TEXT");
b.ToTable("JellyfinSeason", (string)null);
b.ToTable("EmbySeason", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinShow", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinSeason", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Show");
b.HasBaseType("ErsatzTV.Core.Domain.Season");
b.Property<string>("Etag")
.HasColumnType("TEXT");
@ -2591,12 +2634,12 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2591,12 +2634,12 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("ItemId")
.HasColumnType("TEXT");
b.ToTable("JellyfinShow", (string)null);
b.ToTable("JellyfinSeason", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Episode");
b.HasBaseType("ErsatzTV.Core.Domain.Season");
b.Property<string>("Etag")
.HasColumnType("TEXT");
@ -2604,33 +2647,33 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2604,33 +2647,33 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("Key")
.HasColumnType("TEXT");
b.ToTable("PlexEpisode", (string)null);
b.ToTable("PlexSeason", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyShow", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Movie");
b.HasBaseType("ErsatzTV.Core.Domain.Show");
b.Property<string>("Etag")
.HasColumnType("TEXT");
b.Property<string>("Key")
b.Property<string>("ItemId")
.HasColumnType("TEXT");
b.ToTable("PlexMovie", (string)null);
b.ToTable("EmbyShow", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinShow", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Season");
b.HasBaseType("ErsatzTV.Core.Domain.Show");
b.Property<string>("Etag")
.HasColumnType("TEXT");
b.Property<string>("Key")
b.Property<string>("ItemId")
.HasColumnType("TEXT");
b.ToTable("PlexSeason", (string)null);
b.ToTable("JellyfinShow", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b =>
@ -3371,12 +3414,31 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -3371,12 +3414,31 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("PlexMediaSource");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleAlternate", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection")
.WithMany()
.HasForeignKey("CollectionId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("ProgramScheduleAlternates")
.HasForeignKey("PlayoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
.WithMany("ProgramScheduleAlternates")
.HasForeignKey("ProgramScheduleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Playout");
b.Navigation("ProgramSchedule");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection")
.WithMany()
.HasForeignKey("CollectionId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "FallbackFiller")
.WithMany()
@ -3675,15 +3737,6 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -3675,15 +3737,6 @@ namespace ErsatzTV.Infrastructure.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.Artist", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Library", null)
@ -3693,66 +3746,66 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -3693,66 +3746,66 @@ namespace ErsatzTV.Infrastructure.Migrations
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
b.HasOne("ErsatzTV.Core.Domain.Library", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.EmbyMediaSource", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinLibrary", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
b.HasOne("ErsatzTV.Core.Domain.Library", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.Episode", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Season", "Season")
.WithMany("Episodes")
.HasForeignKey("SeasonId")
.HasForeignKey("ErsatzTV.Core.Domain.LocalLibrary", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Season");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Library", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinLibrary", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.PlexLibrary", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
b.HasOne("ErsatzTV.Core.Domain.MediaFile", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinMediaSource", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.PlexMediaFile", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Library", null)
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.LocalLibrary", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.Artist", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.Episode", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Season", "Season")
.WithMany("Episodes")
.HasForeignKey("SeasonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Season");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b =>
@ -3790,100 +3843,109 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -3790,100 +3843,109 @@ namespace ErsatzTV.Infrastructure.Migrations
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Library", null)
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.PlexLibrary", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.Season", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Show", "Show")
.WithMany("Seasons")
.HasForeignKey("ShowId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Show");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaFile", null)
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.PlexMediaFile", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.Show", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.PlexMediaSource", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.Song", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.EmbyMediaSource", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinMediaSource", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemOne", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.PlexMediaSource", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.Season", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
b.HasOne("ErsatzTV.Core.Domain.Show", "Show")
.WithMany("Seasons")
.HasForeignKey("ShowId")
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b =>
{
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Show");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.Show", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.Song", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemOne", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
@ -3897,92 +3959,92 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -3897,92 +3959,92 @@ namespace ErsatzTV.Infrastructure.Migrations
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMovie", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinEpisode", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Movie", null)
b.HasOne("ErsatzTV.Core.Domain.Episode", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.EmbyMovie", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinEpisode", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbySeason", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Season", null)
b.HasOne("ErsatzTV.Core.Domain.Episode", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.EmbySeason", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.PlexEpisode", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyShow", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMovie", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Show", null)
b.HasOne("ErsatzTV.Core.Domain.Movie", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.EmbyShow", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.EmbyMovie", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinEpisode", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMovie", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Episode", null)
b.HasOne("ErsatzTV.Core.Domain.Movie", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinEpisode", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinMovie", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMovie", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Movie", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinMovie", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.PlexMovie", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinSeason", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbySeason", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Season", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinSeason", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.EmbySeason", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinShow", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinSeason", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Show", null)
b.HasOne("ErsatzTV.Core.Domain.Season", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinShow", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinSeason", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Episode", null)
b.HasOne("ErsatzTV.Core.Domain.Season", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.PlexEpisode", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.PlexSeason", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyShow", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Movie", null)
b.HasOne("ErsatzTV.Core.Domain.Show", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.PlexMovie", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.EmbyShow", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinShow", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Season", null)
b.HasOne("ErsatzTV.Core.Domain.Show", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.PlexSeason", "Id")
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinShow", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
@ -4157,6 +4219,8 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -4157,6 +4219,8 @@ namespace ErsatzTV.Infrastructure.Migrations
{
b.Navigation("Items");
b.Navigation("ProgramScheduleAlternates");
b.Navigation("ProgramScheduleAnchors");
});
@ -4165,6 +4229,8 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -4165,6 +4229,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Items");
b.Navigation("Playouts");
b.Navigation("ProgramScheduleAlternates");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b =>
@ -4233,23 +4299,21 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -4233,23 +4299,21 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Guids");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b =>
{
b.Navigation("ArtistMetadata");
b.Navigation("MusicVideos");
b.Navigation("PathInfos");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b =>
{
b.Navigation("PathInfos");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b =>
modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b =>
{
b.Navigation("Connections");
b.Navigation("ArtistMetadata");
b.Navigation("PathReplacements");
b.Navigation("MusicVideos");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b =>
@ -4259,18 +4323,6 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -4259,18 +4323,6 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("MediaVersions");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b =>
{
b.Navigation("PathInfos");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b =>
{
b.Navigation("Connections");
b.Navigation("PathReplacements");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b =>
{
b.Navigation("MediaVersions");
@ -4292,13 +4344,6 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -4292,13 +4344,6 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("OtherVideoMetadata");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
{
b.Navigation("Connections");
b.Navigation("PathReplacements");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b =>
{
b.Navigation("Episodes");
@ -4319,6 +4364,27 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -4319,6 +4364,27 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("SongMetadata");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b =>
{
b.Navigation("Connections");
b.Navigation("PathReplacements");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b =>
{
b.Navigation("Connections");
b.Navigation("PathReplacements");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
{
b.Navigation("Connections");
b.Navigation("PathReplacements");
});
#pragma warning restore 612, 618
}
}

486
ErsatzTV/Pages/PlayoutAlternateSchedulesEditor.razor

@ -0,0 +1,486 @@ @@ -0,0 +1,486 @@
@page "/playouts/{Id:int}/alternate-schedules"
@using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Application.Playouts
@using Microsoft.AspNetCore.Components
@using ErsatzTV.Application.Channels
@using System.Text
@using System.Globalization
@implements IDisposable
@inject NavigationManager NavigationManager
@inject ILogger<ScheduleItemsEditor> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable Hover="true" Items="_items.OrderBy(i => i.Index)" Dense="true" @bind-SelectedItem="_selectedItem">
<ToolBarContent>
<MudText Typo="Typo.h6">@_channelName Alternate Schedules</MudText>
<MudSpacer />
<MudText Typo="Typo.subtitle1" Class="mr-3">In priority order from top to bottom</MudText>
</ToolBarContent>
<ColGroup>
<col/>
<col/>
<col/>
<col/>
<col style="width: 60px;"/>
<col style="width: 60px;"/>
<col style="width: 60px;"/>
</ColGroup>
<HeaderContent>
<MudTh>Schedule</MudTh>
<MudTh>Days of the Week</MudTh>
<MudTh>Days of the Month</MudTh>
<MudTh>Months</MudTh>
<MudTh/>
<MudTh/>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Schedule">
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
@context.ProgramSchedule.Name
</MudText>
</MudTd>
<MudTd DataLabel="Days of the Week">
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
@ToDaysOfWeekString(context.DaysOfWeek)
</MudText>
</MudTd>
<MudTd DataLabel="Days of the Month">
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
@ToDaysOfMonthString(context.DaysOfMonth)
</MudText>
</MudTd>
<MudTd DataLabel="Months">
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
@ToMonthsOfYearString(context.MonthsOfYear)
</MudText>
</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.ArrowUpward"
OnClick="@(_ => MoveItemUp(context))"
Disabled="@(_items.All(x => x.Index >= context.Index))">
</MudIconButton>
</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.ArrowDownward"
OnClick="@(_ => MoveItemDown(context))"
Disabled="@(_items.All(x => x.Index <= context.Index))">
</MudIconButton>
</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => RemoveAlternateSchedule(context))"
Disabled="@(_items.Count == 1)">
</MudIconButton>
</MudTd>
</RowTemplate>
</MudTable>
<MudButton Variant="Variant.Filled" Color="Color.Default" OnClick="@(_ => AddAlternateSchedule())" Class="mt-4">
Add Alternate Schedule
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4 ml-4">
Save Changes
</MudButton>
@if (_selectedItem is not null)
{
<EditForm Model="_selectedItem">
<FluentValidator/>
<div style="display: flex; flex-direction: row;" class="mt-6">
<div style="flex-grow: 1; max-width: 400px;" class="mr-6">
<MudCard>
<MudCardContent>
<MudSelect Label="Schedule" @bind-Value="_selectedItem.ProgramSchedule" For="@(() => _selectedItem.ProgramSchedule)">
@foreach (ProgramScheduleViewModel schedule in _schedules)
{
<MudSelectItem Value="@schedule">@schedule.Name</MudSelectItem>
}
</MudSelect>
</MudCardContent>
</MudCard>
<MudCard Class="mt-4">
<MudCardContent>
<MudElement HtmlTag="div" Class="mt-3">
<MudCheckBox T="bool" Label="@_dtf.GetDayName(DayOfWeek.Monday)"
Checked="@(_selectedItem.DaysOfWeek.Contains(DayOfWeek.Monday))"
CheckedChanged="@(c => DayOfWeekChanged(DayOfWeek.Monday, c))"/>
</MudElement>
<MudElement HtmlTag="div" Class="mt-2">
<MudCheckBox T="bool" Label="@_dtf.GetDayName(DayOfWeek.Tuesday)"
Checked="@(_selectedItem.DaysOfWeek.Contains(DayOfWeek.Tuesday))"
CheckedChanged="@(c => DayOfWeekChanged(DayOfWeek.Tuesday, c))"/>
</MudElement>
<MudElement HtmlTag="div" Class="mt-2">
<MudCheckBox T="bool" Label="@_dtf.GetDayName(DayOfWeek.Wednesday)"
Checked="@(_selectedItem.DaysOfWeek.Contains(DayOfWeek.Wednesday))"
CheckedChanged="@(c => DayOfWeekChanged(DayOfWeek.Wednesday, c))"/>
</MudElement>
<MudElement HtmlTag="div" Class="mt-2">
<MudCheckBox T="bool" Label="@_dtf.GetDayName(DayOfWeek.Thursday)"
Checked="@(_selectedItem.DaysOfWeek.Contains(DayOfWeek.Thursday))"
CheckedChanged="@(c => DayOfWeekChanged(DayOfWeek.Thursday, c))"/>
</MudElement>
<MudElement HtmlTag="div" Class="mt-2">
<MudCheckBox T="bool" Label="@_dtf.GetDayName(DayOfWeek.Friday)"
Checked="@(_selectedItem.DaysOfWeek.Contains(DayOfWeek.Friday))"
CheckedChanged="@(c => DayOfWeekChanged(DayOfWeek.Friday, c))"/>
</MudElement>
<MudElement HtmlTag="div" Class="mt-2">
<MudCheckBox T="bool" Label="@_dtf.GetDayName(DayOfWeek.Saturday)"
Checked="@(_selectedItem.DaysOfWeek.Contains(DayOfWeek.Saturday))"
CheckedChanged="@(c => DayOfWeekChanged(DayOfWeek.Saturday, c))"/>
</MudElement>
<MudElement HtmlTag="div" Class="mt-2">
<MudCheckBox T="bool" Label="@_dtf.GetDayName(DayOfWeek.Sunday)"
Checked="@(_selectedItem.DaysOfWeek.Contains(DayOfWeek.Sunday))"
CheckedChanged="@(c => DayOfWeekChanged(DayOfWeek.Sunday, c))"/>
</MudElement>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="@(_ => SelectWeekdays())">
Weekdays
</MudButton>
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="@(_ => SelectWeekends())">
Weekends
</MudButton>
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="@(_ => SelectAllDaysOfWeek())">
All
</MudButton>
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="@(_ => SelectNoDaysOfWeek())">
None
</MudButton>
</MudCardActions>
</MudCard>
</div>
<div style="flex-grow: 1; max-width: 400px;" class="mr-6">
<MudCard>
<MudCardContent>
<MudGrid Justify="Justify.FlexStart" Class="mt-3">
@foreach (int day in Enumerable.Range(1, 31))
{
<MudItem xs="3">
<MudCheckBox T="bool" Label="@day.ToString()"
Checked="@(_selectedItem.DaysOfMonth.Contains(day))"
CheckedChanged="@(c => DayOfMonthChanged(day, c))"/>
</MudItem>
}
</MudGrid>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="@(_ => SelectAllDaysOfMonth())">
All
</MudButton>
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="@(_ => SelectNoDaysOfMonth())">
None
</MudButton>
</MudCardActions>
</MudCard>
</div>
<div style="flex-grow: 1; max-width: 400px;">
<MudCard>
<MudCardContent>
<MudElement HtmlTag="div" Class="mt-3">
<MudCheckBox T="bool" Label="@_dtf.GetMonthName(1)"
Checked="@(_selectedItem.MonthsOfYear.Contains(1))"
CheckedChanged="@(c => MonthOfYearChanged(1, c))"/>
</MudElement>
@foreach (int month in Enumerable.Range(2, 11))
{
<MudElement HtmlTag="div" Class="mt-2">
<MudCheckBox T="bool" Label="@_dtf.GetMonthName(month)"
Checked="@(_selectedItem.MonthsOfYear.Contains(month))"
CheckedChanged="@(c => MonthOfYearChanged(month, c))"/>
</MudElement>
}
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="@(_ => SelectAllMonthsOfYear())">
All
</MudButton>
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="@(_ => SelectNoMonthsOfYear())">
None
</MudButton>
</MudCardActions>
</MudCard>
</div>
</div>
</EditForm>
}
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
private readonly DateTimeFormatInfo _dtf = CultureInfo.CurrentUICulture.DateTimeFormat;
[Parameter]
public int Id { get; set; }
private string _channelName;
private List<PlayoutAlternateScheduleEditViewModel> _items;
private List<ProgramScheduleViewModel> _schedules;
private PlayoutAlternateScheduleEditViewModel _selectedItem;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync() => await LoadScheduleItems();
private async Task LoadScheduleItems()
{
_schedules = await Mediator.Send(new GetAllProgramSchedules(), _cts.Token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
_channelName = (await Mediator.Send(new GetChannelNameByPlayoutId(Id), _cts.Token)).IfNone(string.Empty);
List<PlayoutAlternateScheduleViewModel> results = await Mediator.Send(new GetPlayoutAlternateSchedules(Id), _cts.Token);
_items = results.Map(ProjectToEditViewModel).ToList();
if (_items.Count == 1)
{
_selectedItem = _items.Head();
}
}
private PlayoutAlternateScheduleEditViewModel ProjectToEditViewModel(PlayoutAlternateScheduleViewModel item) =>
new()
{
Id = item.Id,
Index = item.Index,
ProgramSchedule = _schedules.Find(vm => vm.Id == item.ProgramScheduleId),
DaysOfWeek = item.DaysOfWeek.OrderBy(x => ((int)x + 6) % 7).ToList(),
DaysOfMonth = item.DaysOfMonth.ToList(),
MonthsOfYear = item.MonthsOfYear.ToList()
};
private void DayOfWeekChanged(DayOfWeek dayOfWeek, bool isChecked)
{
if (isChecked && !_selectedItem.DaysOfWeek.Contains(dayOfWeek))
{
_selectedItem.DaysOfWeek.Add(dayOfWeek);
_selectedItem.DaysOfWeek = _selectedItem.DaysOfWeek.OrderBy(x => ((int)x + 6) % 7).ToList();
}
if (!isChecked)
{
_selectedItem.DaysOfWeek.Remove(dayOfWeek);
}
}
private void SelectWeekdays()
{
_selectedItem.DaysOfWeek.Clear();
_selectedItem.DaysOfWeek.AddRange(new[]
{
DayOfWeek.Monday,
DayOfWeek.Tuesday,
DayOfWeek.Wednesday,
DayOfWeek.Thursday,
DayOfWeek.Friday
});
}
private void SelectWeekends()
{
_selectedItem.DaysOfWeek.Clear();
_selectedItem.DaysOfWeek.AddRange(new[]
{
DayOfWeek.Saturday,
DayOfWeek.Sunday
});
}
private void SelectAllDaysOfWeek()
{
_selectedItem.DaysOfWeek.Clear();
_selectedItem.DaysOfWeek.AddRange(ProgramScheduleAlternate.AllDaysOfWeek());
}
private void SelectNoDaysOfWeek()
{
_selectedItem.DaysOfWeek.Clear();
}
private void DayOfMonthChanged(int dayOfMonth, bool isChecked)
{
if (isChecked && !_selectedItem.DaysOfMonth.Contains(dayOfMonth))
{
_selectedItem.DaysOfMonth.Add(dayOfMonth);
_selectedItem.DaysOfMonth.Sort();
}
if (!isChecked)
{
_selectedItem.DaysOfMonth.Remove(dayOfMonth);
}
}
private void SelectAllDaysOfMonth()
{
_selectedItem.DaysOfMonth.Clear();
_selectedItem.DaysOfMonth.AddRange(ProgramScheduleAlternate.AllDaysOfMonth());
}
private void SelectNoDaysOfMonth()
{
_selectedItem.DaysOfMonth.Clear();
}
private void MonthOfYearChanged(int monthOfYear, bool isChecked)
{
if (isChecked && !_selectedItem.MonthsOfYear.Contains(monthOfYear))
{
_selectedItem.MonthsOfYear.Add(monthOfYear);
_selectedItem.MonthsOfYear.Sort();
}
if (!isChecked)
{
_selectedItem.MonthsOfYear.Remove(monthOfYear);
}
}
private void SelectAllMonthsOfYear()
{
_selectedItem.MonthsOfYear.Clear();
_selectedItem.MonthsOfYear.AddRange(ProgramScheduleAlternate.AllMonthsOfYear());
}
private void SelectNoMonthsOfYear()
{
_selectedItem.MonthsOfYear.Clear();
}
private void AddAlternateSchedule()
{
var item = new PlayoutAlternateScheduleEditViewModel
{
Index = _items.Map(i => i.Index).DefaultIfEmpty().Max() + 1,
ProgramSchedule = _schedules.Head(),
DaysOfWeek = ProgramScheduleAlternate.AllDaysOfWeek(),
DaysOfMonth = ProgramScheduleAlternate.AllDaysOfMonth(),
MonthsOfYear = ProgramScheduleAlternate.AllMonthsOfYear()
};
_items.Add(item);
_selectedItem = item;
}
private void RemoveAlternateSchedule(PlayoutAlternateScheduleEditViewModel item)
{
_selectedItem = null;
_items.Remove(item);
}
private void MoveItemUp(PlayoutAlternateScheduleEditViewModel item)
{
// swap with lower index
PlayoutAlternateScheduleEditViewModel toSwap = _items.OrderByDescending(x => x.Index).First(x => x.Index < item.Index);
(toSwap.Index, item.Index) = (item.Index, toSwap.Index);
}
private void MoveItemDown(PlayoutAlternateScheduleEditViewModel item)
{
// swap with higher index
PlayoutAlternateScheduleEditViewModel toSwap = _items.OrderBy(x => x.Index).First(x => x.Index > item.Index);
(toSwap.Index, item.Index) = (item.Index, toSwap.Index);
}
private async Task SaveChanges()
{
var items = _items.Map(item => new ReplacePlayoutAlternateSchedule(
item.Id,
item.Index,
item.ProgramSchedule.Id,
item.DaysOfWeek,
item.DaysOfMonth,
item.MonthsOfYear)).ToList();
Seq<BaseError> errorMessages = await Mediator.Send(new ReplacePlayoutAlternateScheduleItems(Id, items), _cts.Token)
.Map(e => e.LeftToSeq());
errorMessages.HeadOrNone().Match(
error =>
{
Snackbar.Add($"Unexpected error saving alternate schedules: {error.Value}", Severity.Error);
Logger.LogError("Unexpected error saving alternate schedules: {Error}", error.Value);
},
() => NavigationManager.NavigateTo("/playouts"));
}
private string ToDaysOfWeekString(List<DayOfWeek> daysOfWeek)
{
if (daysOfWeek.Count is 0 or 7)
{
return "*any*";
}
daysOfWeek.Sort();
return string.Join(", ", daysOfWeek.Map(_dtf.GetAbbreviatedDayName));
}
private string ToDaysOfMonthString(List<int> daysOfMonth)
{
if (daysOfMonth.Count is 0 or 31)
{
return "*any*";
}
return ToRangeString(daysOfMonth);
}
private string ToMonthsOfYearString(List<int> monthsOfYear)
{
if (monthsOfYear.Count is 0 or 12)
{
return "*any*";
}
monthsOfYear.Sort();
return string.Join(", ", monthsOfYear.Map(_dtf.GetAbbreviatedMonthName));
}
private static string ToRangeString(List<int> list)
{
list = list.Distinct().ToList();
list.Sort();
var result = new StringBuilder();
for (var i = 0; i < list.Count; i++)
{
int temp = list[i];
//add a number
result.Append(list[i]);
//skip number(s) between a range
while (i < list.Count - 1 && list[i + 1] == list[i] + 1)
{
i++;
}
//add the range
if (temp != list[i])
{
result.Append("-").Append(list[i]);
}
//add comma
if (i != list.Count - 1)
{
result.Append(", ");
}
}
return result.ToString();
}
}

7
ErsatzTV/Pages/Playouts.razor

@ -33,7 +33,7 @@ @@ -33,7 +33,7 @@
</MudTh>
<MudTh>
<MudTableSortLabel SortBy="new Func<PlayoutViewModel, object>(x => x.ProgramSchedule.Name)">
Schedule
Default Schedule
</MudTableSortLabel>
</MudTh>
@* <MudTh>Playout Type</MudTh> *@
@ -45,6 +45,11 @@ @@ -45,6 +45,11 @@
@* <MudTd DataLabel="Playout Type">@context.ProgramSchedulePlayoutType</MudTd> *@
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Edit Alternate Schedules">
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar"
Link="@($"playouts/{context.PlayoutId}/alternate-schedules")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Reset Playout">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
OnClick="@(_ => ResetPlayout(context))">

12
ErsatzTV/Validators/PlayoutAlternateScheduleEditViewModelValidator.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
using ErsatzTV.ViewModels;
using FluentValidation;
namespace ErsatzTV.Validators;
public class PlayoutAlternateScheduleEditViewModelValidator : AbstractValidator<PlayoutAlternateScheduleEditViewModel>
{
public PlayoutAlternateScheduleEditViewModelValidator()
{
RuleFor(p => p.ProgramSchedule).NotNull();
}
}

13
ErsatzTV/ViewModels/PlayoutAlternateScheduleEditViewModel.cs

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
using ErsatzTV.Application.ProgramSchedules;
namespace ErsatzTV.ViewModels;
public class PlayoutAlternateScheduleEditViewModel
{
public int Id { get; set; }
public int Index { get; set; }
public ProgramScheduleViewModel ProgramSchedule { get; set; }
public List<DayOfWeek> DaysOfWeek { get; set; }
public List<int> DaysOfMonth { get; set; }
public List<int> MonthsOfYear { get; set; }
}
Loading…
Cancel
Save