Browse Source

add fixed start time behavior option to schedules and schedule items (#2017)

pull/2020/head
Jason Dove 2 months ago committed by GitHub
parent
commit
d9a3496bf5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      CHANGELOG.md
  2. 2
      ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs
  3. 4
      ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramSchedule.cs
  4. 12
      ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleHandler.cs
  5. 2
      ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs
  6. 4
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  7. 2
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs
  8. 4
      ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramSchedule.cs
  9. 4
      ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs
  10. 7
      ErsatzTV.Application/ProgramSchedules/Mapper.cs
  11. 3
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs
  12. 3
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs
  13. 3
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs
  14. 3
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs
  15. 2
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs
  16. 7
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleViewModel.cs
  17. 13
      ErsatzTV.Application/ProgramSchedules/Queries/GetAllProgramSchedulesHandler.cs
  18. 5
      ErsatzTV.Core/Domain/ProgramSchedule.cs
  19. 2
      ErsatzTV.Core/Domain/ProgramScheduleItem.cs
  20. 7
      ErsatzTV.Core/Scheduling/FixedStartTimeBehavior.cs
  21. 16
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs
  22. 5878
      ErsatzTV.Infrastructure.MySql/Migrations/20250530083135_Add_ProgramSchedule_FixedStartTimeBehavior.Designer.cs
  23. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20250530083135_Add_ProgramSchedule_FixedStartTimeBehavior.cs
  24. 5881
      ErsatzTV.Infrastructure.MySql/Migrations/20250530085651_Add_ProgramScheduleItem_FixedStartTimeBehavior.Designer.cs
  25. 28
      ErsatzTV.Infrastructure.MySql/Migrations/20250530085651_Add_ProgramScheduleItem_FixedStartTimeBehavior.cs
  26. 8
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  27. 5717
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250530083219_Add_ProgramSchedule_FixedStartTimeBehavior.Designer.cs
  28. 29
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250530083219_Add_ProgramSchedule_FixedStartTimeBehavior.cs
  29. 5720
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250530085731_Add_ProgramScheduleItem_FixedStartTimeBehavior.Designer.cs
  30. 28
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250530085731_Add_ProgramScheduleItem_FixedStartTimeBehavior.cs
  31. 8
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  32. 2
      ErsatzTV/Pages/Artist.razor
  33. 2
      ErsatzTV/Pages/Playouts.razor
  34. 10
      ErsatzTV/Pages/ScheduleEditor.razor
  35. 11
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  36. 2
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  37. 2
      ErsatzTV/Pages/TelevisionSeasonList.razor
  38. 19
      ErsatzTV/ViewModels/ProgramScheduleEditViewModel.cs
  39. 8
      ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

9
CHANGELOG.md

@ -13,6 +13,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -13,6 +13,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `ETV_STREAMING_PORT`: port used for streaming requests, defaults to 8409
- `ETV_UI_PORT`: port used for admin UI, defaults to 8409
- Publish docker images to ghcr.io (`ghcr.io/ersatztv/ersatztv`)
- Add new option `Fixed Start Time Behavior` to Schedules and Schedule Items
- Schedules can set a default behavior for all items
- Schedule items can override this default behavior
- Possible values are:
- `Strict`: Always wait for the exact start time, even if that means waiting (adding unscheduled time) until the next day
- `Flexible`: Start scheduling immediately (do not wait) if waiting (adding unscheduled time) would go into the next day
- As an example, if the current scheduling time is 6:02 AM and the next schedule item has a fixed start time of 6:00 AM
- `Strict` will add nearly 24h (23:58) of unscheduled time so that it can start exactly at 6:00 AM the next day
- `Flexible` will NOT add unscheduled time, and will schedule its item at 6:02 AM (which may also affect the scheduling of later items)
### Changed
- Start to make UI minimally responsive (functional on smaller screens)

2
ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Application.ProgramSchedules;
@ -7,6 +8,7 @@ public record AddProgramScheduleItem( @@ -7,6 +8,7 @@ public record AddProgramScheduleItem(
int ProgramScheduleId,
StartType StartType,
TimeSpan? StartTime,
FixedStartTimeBehavior? FixedStartTimeBehavior,
PlayoutMode PlayoutMode,
ProgramScheduleItemCollectionType CollectionType,
int? CollectionId,

4
ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramSchedule.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Application.ProgramSchedules;
@ -7,4 +8,5 @@ public record CreateProgramSchedule( @@ -7,4 +8,5 @@ public record CreateProgramSchedule(
bool KeepMultiPartEpisodesTogether,
bool TreatCollectionsAsShows,
bool ShuffleScheduleItems,
bool RandomStartPoint) : IRequest<Either<BaseError, CreateProgramScheduleResult>>;
bool RandomStartPoint,
FixedStartTimeBehavior FixedStartTimeBehavior) : IRequest<Either<BaseError, CreateProgramScheduleResult>>;

12
ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleHandler.cs

@ -5,19 +5,14 @@ using Microsoft.EntityFrameworkCore; @@ -5,19 +5,14 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.ProgramSchedules;
public class CreateProgramScheduleHandler :
public class CreateProgramScheduleHandler(IDbContextFactory<TvContext> dbContextFactory) :
IRequestHandler<CreateProgramSchedule, Either<BaseError, CreateProgramScheduleResult>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateProgramScheduleHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, CreateProgramScheduleResult>> Handle(
CreateProgramSchedule request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
return await validation.Apply(ps => PersistProgramSchedule(dbContext, ps));
@ -45,7 +40,8 @@ public class CreateProgramScheduleHandler : @@ -45,7 +40,8 @@ public class CreateProgramScheduleHandler :
KeepMultiPartEpisodesTogether = keepMultiPartEpisodesTogether,
TreatCollectionsAsShows = keepMultiPartEpisodesTogether && request.TreatCollectionsAsShows,
ShuffleScheduleItems = request.ShuffleScheduleItems,
RandomStartPoint = request.RandomStartPoint
RandomStartPoint = request.RandomStartPoint,
FixedStartTimeBehavior = request.FixedStartTimeBehavior
};
});

2
ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs

@ -1,10 +1,12 @@ @@ -1,10 +1,12 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Application.ProgramSchedules;
public interface IProgramScheduleItemRequest
{
TimeSpan? StartTime { get; }
FixedStartTimeBehavior? FixedStartTimeBehavior { get; }
ProgramScheduleItemCollectionType CollectionType { get; }
int? CollectionId { get; }
int? MultiCollectionId { get; }

4
ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs

@ -185,6 +185,7 @@ public abstract class ProgramScheduleItemCommandBase @@ -185,6 +185,7 @@ public abstract class ProgramScheduleItemCommandBase
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = FixStartTime(item.StartTime),
FixedStartTimeBehavior = item.FixedStartTimeBehavior,
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId,
@ -211,6 +212,7 @@ public abstract class ProgramScheduleItemCommandBase @@ -211,6 +212,7 @@ public abstract class ProgramScheduleItemCommandBase
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = FixStartTime(item.StartTime),
FixedStartTimeBehavior = item.FixedStartTimeBehavior,
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId,
@ -237,6 +239,7 @@ public abstract class ProgramScheduleItemCommandBase @@ -237,6 +239,7 @@ public abstract class ProgramScheduleItemCommandBase
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = FixStartTime(item.StartTime),
FixedStartTimeBehavior = item.FixedStartTimeBehavior,
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId,
@ -264,6 +267,7 @@ public abstract class ProgramScheduleItemCommandBase @@ -264,6 +267,7 @@ public abstract class ProgramScheduleItemCommandBase
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = FixStartTime(item.StartTime),
FixedStartTimeBehavior = item.FixedStartTimeBehavior,
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId,

2
ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Application.ProgramSchedules;
@ -7,6 +8,7 @@ public record ReplaceProgramScheduleItem( @@ -7,6 +8,7 @@ public record ReplaceProgramScheduleItem(
int Index,
StartType StartType,
TimeSpan? StartTime,
FixedStartTimeBehavior? FixedStartTimeBehavior,
PlayoutMode PlayoutMode,
ProgramScheduleItemCollectionType CollectionType,
int? CollectionId,

4
ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramSchedule.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Application.ProgramSchedules;
@ -8,4 +9,5 @@ public record UpdateProgramSchedule( @@ -8,4 +9,5 @@ public record UpdateProgramSchedule(
bool KeepMultiPartEpisodesTogether,
bool TreatCollectionsAsShows,
bool ShuffleScheduleItems,
bool RandomStartPoint) : IRequest<Either<BaseError, UpdateProgramScheduleResult>>;
bool RandomStartPoint,
FixedStartTimeBehavior FixedStartTimeBehavior) : IRequest<Either<BaseError, UpdateProgramScheduleResult>>;

4
ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs

@ -42,7 +42,8 @@ public class UpdateProgramScheduleHandler : @@ -42,7 +42,8 @@ public class UpdateProgramScheduleHandler :
programSchedule.KeepMultiPartEpisodesTogether != request.KeepMultiPartEpisodesTogether ||
programSchedule.TreatCollectionsAsShows != request.TreatCollectionsAsShows ||
programSchedule.ShuffleScheduleItems != request.ShuffleScheduleItems ||
programSchedule.RandomStartPoint != request.RandomStartPoint;
programSchedule.RandomStartPoint != request.RandomStartPoint ||
programSchedule.FixedStartTimeBehavior != request.FixedStartTimeBehavior;
programSchedule.Name = request.Name;
programSchedule.KeepMultiPartEpisodesTogether = request.KeepMultiPartEpisodesTogether;
@ -50,6 +51,7 @@ public class UpdateProgramScheduleHandler : @@ -50,6 +51,7 @@ public class UpdateProgramScheduleHandler :
request.TreatCollectionsAsShows;
programSchedule.ShuffleScheduleItems = request.ShuffleScheduleItems;
programSchedule.RandomStartPoint = request.RandomStartPoint;
programSchedule.FixedStartTimeBehavior = request.FixedStartTimeBehavior;
await dbContext.SaveChangesAsync();

7
ErsatzTV.Application/ProgramSchedules/Mapper.cs

@ -11,7 +11,8 @@ internal static class Mapper @@ -11,7 +11,8 @@ internal static class Mapper
programSchedule.KeepMultiPartEpisodesTogether,
programSchedule.TreatCollectionsAsShows,
programSchedule.ShuffleScheduleItems,
programSchedule.RandomStartPoint);
programSchedule.RandomStartPoint,
programSchedule.FixedStartTimeBehavior);
internal static ProgramScheduleItemViewModel ProjectToViewModel(ProgramScheduleItem programScheduleItem) =>
programScheduleItem switch
@ -22,6 +23,7 @@ internal static class Mapper @@ -22,6 +23,7 @@ internal static class Mapper
duration.Index,
duration.StartType,
duration.StartTime,
duration.FixedStartTimeBehavior,
duration.CollectionType,
duration.Collection != null
? MediaCollections.Mapper.ProjectToViewModel(duration.Collection)
@ -77,6 +79,7 @@ internal static class Mapper @@ -77,6 +79,7 @@ internal static class Mapper
flood.Index,
flood.StartType,
flood.StartTime,
flood.FixedStartTimeBehavior,
flood.CollectionType,
flood.Collection != null
? MediaCollections.Mapper.ProjectToViewModel(flood.Collection)
@ -129,6 +132,7 @@ internal static class Mapper @@ -129,6 +132,7 @@ internal static class Mapper
multiple.Index,
multiple.StartType,
multiple.StartTime,
multiple.FixedStartTimeBehavior,
multiple.CollectionType,
multiple.Collection != null
? MediaCollections.Mapper.ProjectToViewModel(multiple.Collection)
@ -182,6 +186,7 @@ internal static class Mapper @@ -182,6 +186,7 @@ internal static class Mapper
one.Index,
one.StartType,
one.StartTime,
one.FixedStartTimeBehavior,
one.CollectionType,
one.Collection != null
? MediaCollections.Mapper.ProjectToViewModel(one.Collection)

3
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs

@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections; @@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Application.ProgramSchedules;
@ -13,6 +14,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode @@ -13,6 +14,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
int index,
StartType startType,
TimeSpan? startTime,
FixedStartTimeBehavior? fixedStartTimeBehavior,
ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel collection,
MultiCollectionViewModel multiCollection,
@ -40,6 +42,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode @@ -40,6 +42,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
index,
startType,
startTime,
fixedStartTimeBehavior,
PlayoutMode.Duration,
collectionType,
collection,

3
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs

@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections; @@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Application.ProgramSchedules;
@ -13,6 +14,7 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel @@ -13,6 +14,7 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
int index,
StartType startType,
TimeSpan? startTime,
FixedStartTimeBehavior? fixedStartTimeBehavior,
ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel collection,
MultiCollectionViewModel multiCollection,
@ -37,6 +39,7 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel @@ -37,6 +39,7 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
index,
startType,
startTime,
fixedStartTimeBehavior,
PlayoutMode.Flood,
collectionType,
collection,

3
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs

@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections; @@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Application.ProgramSchedules;
@ -13,6 +14,7 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode @@ -13,6 +14,7 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
int index,
StartType startType,
TimeSpan? startTime,
FixedStartTimeBehavior? fixedStartTimeBehavior,
ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel collection,
MultiCollectionViewModel multiCollection,
@ -38,6 +40,7 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode @@ -38,6 +40,7 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
index,
startType,
startTime,
fixedStartTimeBehavior,
PlayoutMode.Multiple,
collectionType,
collection,

3
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs

@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections; @@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Application.ProgramSchedules;
@ -13,6 +14,7 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel @@ -13,6 +14,7 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
int index,
StartType startType,
TimeSpan? startTime,
FixedStartTimeBehavior? fixedStartTimeBehavior,
ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel collection,
MultiCollectionViewModel multiCollection,
@ -37,6 +39,7 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel @@ -37,6 +39,7 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
index,
startType,
startTime,
fixedStartTimeBehavior,
PlayoutMode.One,
collectionType,
collection,

2
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs

@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections; @@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Application.ProgramSchedules;
@ -11,6 +12,7 @@ public abstract record ProgramScheduleItemViewModel( @@ -11,6 +12,7 @@ public abstract record ProgramScheduleItemViewModel(
int Index,
StartType StartType,
TimeSpan? StartTime,
FixedStartTimeBehavior? FixedStartTimeBehavior,
PlayoutMode PlayoutMode,
ProgramScheduleItemCollectionType CollectionType,
MediaCollectionViewModel Collection,

7
ErsatzTV.Application/ProgramSchedules/ProgramScheduleViewModel.cs

@ -1,4 +1,6 @@ @@ -1,4 +1,6 @@
namespace ErsatzTV.Application.ProgramSchedules;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Application.ProgramSchedules;
public record ProgramScheduleViewModel(
int Id,
@ -6,4 +8,5 @@ public record ProgramScheduleViewModel( @@ -6,4 +8,5 @@ public record ProgramScheduleViewModel(
bool KeepMultiPartEpisodesTogether,
bool TreatCollectionsAsShows,
bool ShuffleScheduleItems,
bool RandomStartPoint);
bool RandomStartPoint,
FixedStartTimeBehavior FixedStartTimeBehavior);

13
ErsatzTV.Application/ProgramSchedules/Queries/GetAllProgramSchedulesHandler.cs

@ -3,18 +3,14 @@ using Microsoft.EntityFrameworkCore; @@ -3,18 +3,14 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.ProgramSchedules;
public class GetAllProgramSchedulesHandler : IRequestHandler<GetAllProgramSchedules, List<ProgramScheduleViewModel>>
public class GetAllProgramSchedulesHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetAllProgramSchedules, List<ProgramScheduleViewModel>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllProgramSchedulesHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<ProgramScheduleViewModel>> Handle(
GetAllProgramSchedules request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.ProgramSchedules
.Map(
ps => new ProgramScheduleViewModel(
@ -23,7 +19,8 @@ public class GetAllProgramSchedulesHandler : IRequestHandler<GetAllProgramSchedu @@ -23,7 +19,8 @@ public class GetAllProgramSchedulesHandler : IRequestHandler<GetAllProgramSchedu
ps.KeepMultiPartEpisodesTogether,
ps.TreatCollectionsAsShows,
ps.ShuffleScheduleItems,
ps.RandomStartPoint))
ps.RandomStartPoint,
ps.FixedStartTimeBehavior))
.ToListAsync(cancellationToken);
}
}

5
ErsatzTV.Core/Domain/ProgramSchedule.cs

@ -1,4 +1,6 @@ @@ -1,4 +1,6 @@
namespace ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Core.Domain;
public class ProgramSchedule
{
@ -8,6 +10,7 @@ public class ProgramSchedule @@ -8,6 +10,7 @@ public class ProgramSchedule
public bool TreatCollectionsAsShows { get; set; }
public bool ShuffleScheduleItems { get; set; }
public bool RandomStartPoint { get; set; }
public FixedStartTimeBehavior FixedStartTimeBehavior { get; set; }
public List<ProgramScheduleItem> Items { get; set; }
public List<Playout> Playouts { get; set; }
public List<ProgramScheduleAlternate> ProgramScheduleAlternates { get; set; }

2
ErsatzTV.Core/Domain/ProgramScheduleItem.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Scheduling;
using Newtonsoft.Json;
namespace ErsatzTV.Core.Domain;
@ -9,6 +10,7 @@ public abstract class ProgramScheduleItem @@ -9,6 +10,7 @@ public abstract class ProgramScheduleItem
public int Index { get; set; }
public StartType StartType => StartTime.HasValue ? StartType.Fixed : StartType.Dynamic;
public TimeSpan? StartTime { get; set; }
public FixedStartTimeBehavior? FixedStartTimeBehavior { get; set; }
public ProgramScheduleItemCollectionType CollectionType { get; set; }
public GuideMode GuideMode { get; set; }
public string CustomTitle { get; set; }

7
ErsatzTV.Core/Scheduling/FixedStartTimeBehavior.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
namespace ErsatzTV.Core.Scheduling;
public enum FixedStartTimeBehavior
{
Strict = 0,
Flexible = 1
}

16
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs

@ -71,7 +71,21 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe @@ -71,7 +71,21 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
// itemStartTime.TotalMilliseconds);
// need to wrap to the next day if appropriate
startTime = startTime.TimeOfDay > itemStartTime ? result.AddDays(1) : result;
FixedStartTimeBehavior? fixedStartTimeBehavior = scheduleItem.FixedStartTimeBehavior;
if (fixedStartTimeBehavior is null && scheduleItem.ProgramSchedule is not null)
fixedStartTimeBehavior = scheduleItem.ProgramSchedule.FixedStartTimeBehavior;
switch (fixedStartTimeBehavior)
{
case FixedStartTimeBehavior.Flexible:
// only wait for times on the same day
if (result.Day == startTime.Day && result.TimeOfDay > startTime.TimeOfDay)
startTime = result;
break;
case FixedStartTimeBehavior.Strict:
default:
startTime = startTime.TimeOfDay > itemStartTime ? result.AddDays(1) : result;
break;
}
}
return startTime;

5878
ErsatzTV.Infrastructure.MySql/Migrations/20250530083135_Add_ProgramSchedule_FixedStartTimeBehavior.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20250530083135_Add_ProgramSchedule_FixedStartTimeBehavior.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_ProgramSchedule_FixedStartTimeBehavior : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "FixedStartTimeBehavior",
table: "ProgramSchedule",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FixedStartTimeBehavior",
table: "ProgramSchedule");
}
}
}

5881
ErsatzTV.Infrastructure.MySql/Migrations/20250530085651_Add_ProgramScheduleItem_FixedStartTimeBehavior.Designer.cs generated

File diff suppressed because it is too large Load Diff

28
ErsatzTV.Infrastructure.MySql/Migrations/20250530085651_Add_ProgramScheduleItem_FixedStartTimeBehavior.cs

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

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

@ -17,7 +17,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -17,7 +17,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.10")
.HasAnnotation("ProductVersion", "8.0.15")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
@ -1969,6 +1969,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -1969,6 +1969,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<int>("FixedStartTimeBehavior")
.HasColumnType("int");
b.Property<bool>("KeepMultiPartEpisodesTogether")
.HasColumnType("tinyint(1)");
@ -2053,6 +2056,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2053,6 +2056,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int>("FillWithGroupMode")
.HasColumnType("int");
b.Property<int?>("FixedStartTimeBehavior")
.HasColumnType("int");
b.Property<int>("GuideMode")
.HasColumnType("int");

5717
ErsatzTV.Infrastructure.Sqlite/Migrations/20250530083219_Add_ProgramSchedule_FixedStartTimeBehavior.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.Sqlite/Migrations/20250530083219_Add_ProgramSchedule_FixedStartTimeBehavior.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_ProgramSchedule_FixedStartTimeBehavior : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "FixedStartTimeBehavior",
table: "ProgramSchedule",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FixedStartTimeBehavior",
table: "ProgramSchedule");
}
}
}

5720
ErsatzTV.Infrastructure.Sqlite/Migrations/20250530085731_Add_ProgramScheduleItem_FixedStartTimeBehavior.Designer.cs generated

File diff suppressed because it is too large Load Diff

28
ErsatzTV.Infrastructure.Sqlite/Migrations/20250530085731_Add_ProgramScheduleItem_FixedStartTimeBehavior.cs

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

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

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
modelBuilder.HasAnnotation("ProductVersion", "8.0.15");
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
{
@ -1868,6 +1868,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -1868,6 +1868,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("FixedStartTimeBehavior")
.HasColumnType("INTEGER");
b.Property<bool>("KeepMultiPartEpisodesTogether")
.HasColumnType("INTEGER");
@ -1948,6 +1951,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -1948,6 +1951,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("FillWithGroupMode")
.HasColumnType("INTEGER");
b.Property<int?>("FixedStartTimeBehavior")
.HasColumnType("INTEGER");
b.Property<int>("GuideMode")
.HasColumnType("INTEGER");

2
ErsatzTV/Pages/Artist.razor

@ -259,7 +259,7 @@ @@ -259,7 +259,7 @@
DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is ProgramScheduleViewModel schedule)
{
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
NavigationManager.NavigateTo($"schedules/{schedule.Id}/items");
}
}

2
ErsatzTV/Pages/Playouts.razor

@ -250,6 +250,8 @@ @@ -250,6 +250,8 @@
public void Dispose()
{
Courier.UnSubscribe<PlayoutUpdatedNotification>(HandlePlayoutUpdated);
_cts.Cancel();
_cts.Dispose();
}

10
ErsatzTV/Pages/ScheduleEditor.razor

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
@page "/schedules/{Id:int}"
@page "/schedules/add"
@using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Core.Scheduling
@implements IDisposable
@inject NavigationManager NavigationManager
@inject ILogger<ScheduleEditor> Logger
@ -43,6 +44,14 @@ @@ -43,6 +44,14 @@
@bind-Value="@_model.RandomStartPoint"
For="@(() => _model.RandomStartPoint)"/>
</MudElement>
<MudElement HtmlTag="div" Class="mt-3">
<MudSelect Label="Fixed Start Time Behavior"
@bind-Value="@_model.FixedStartTimeBehavior"
For="@(() => _model.FixedStartTimeBehavior)">
<MudSelectItem Value="FixedStartTimeBehavior.Strict">Strict - Always Wait For Exact Start Time</MudSelectItem>
<MudSelectItem Value="FixedStartTimeBehavior.Flexible">Flexible - Start As Soon As Possible After Start Time</MudSelectItem>
</MudSelect>
</MudElement>
</MudCardContent>
<MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary">
@ -84,6 +93,7 @@ @@ -84,6 +93,7 @@
_model.KeepMultiPartEpisodesTogether = viewModel.KeepMultiPartEpisodesTogether;
_model.TreatCollectionsAsShows = viewModel.TreatCollectionsAsShows;
_model.RandomStartPoint = viewModel.RandomStartPoint;
_model.FixedStartTimeBehavior = viewModel.FixedStartTimeBehavior;
},
() => NavigationManager.NavigateTo("404"));
}

11
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
@using ErsatzTV.Application.Search
@using ErsatzTV.Application.Watermarks
@using ErsatzTV.Core.Domain.Filler
@using ErsatzTV.Core.Scheduling
@implements IDisposable
@inject NavigationManager NavigationManager
@inject ILogger<ScheduleItemsEditor> Logger
@ -105,6 +106,14 @@ @@ -105,6 +106,14 @@
}
</MudSelect>
<MudTimePicker Class="mt-3" Label="Start Time" @bind-Time="@_selectedItem.StartTime" For="@(() => _selectedItem.StartTime)" Disabled="@(_selectedItem.StartType == StartType.Dynamic)" Editable="true"/>
@if (_selectedItem.StartType == StartType.Fixed)
{
<MudSelect Class="mt-3" Label="Fixed Start Time Behavior" @bind-Value="_selectedItem.FixedStartTimeBehavior" For="@(() => _selectedItem.FixedStartTimeBehavior)">
<MudSelectItem Value="@((FixedStartTimeBehavior?)null)">Inherit</MudSelectItem>
<MudSelectItem T="FixedStartTimeBehavior?" Value="FixedStartTimeBehavior.Strict">Strict - Always Wait For Exact Start Time</MudSelectItem>
<MudSelectItem T="FixedStartTimeBehavior?" Value="FixedStartTimeBehavior.Flexible">Flexible - Start As Soon As Possible After Start Time</MudSelectItem>
</MudSelect>
}
<MudSelect Class="mt-3" Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)">
<MudSelectItem Value="ProgramScheduleItemCollectionType.Collection">Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionShow">Television Show</MudSelectItem>
@ -526,6 +535,7 @@ @@ -526,6 +535,7 @@
Index = item.Index,
StartType = item.StartType,
StartTime = item.StartTime,
FixedStartTimeBehavior = item.FixedStartTimeBehavior,
PlayoutMode = item.PlayoutMode,
CollectionType = item.CollectionType,
Collection = item.Collection,
@ -606,6 +616,7 @@ @@ -606,6 +616,7 @@
item.Index,
item.StartType,
item.StartTime,
item.FixedStartTimeBehavior,
item.PlayoutMode,
item.CollectionType,
item.Collection?.Id,

2
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -269,7 +269,7 @@ @@ -269,7 +269,7 @@
DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is ProgramScheduleViewModel schedule)
{
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
NavigationManager.NavigateTo($"schedules/{schedule.Id}/items");
}
}

2
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -249,7 +249,7 @@ @@ -249,7 +249,7 @@
DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is ProgramScheduleViewModel schedule)
{
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, null, PlaybackOrder.Shuffle, FillWithGroupMode.None, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
NavigationManager.NavigateTo($"schedules/{schedule.Id}/items");
}
}

19
ErsatzTV/ViewModels/ProgramScheduleEditViewModel.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Application.ProgramSchedules;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.ViewModels;
@ -10,10 +11,24 @@ public class ProgramScheduleEditViewModel @@ -10,10 +11,24 @@ public class ProgramScheduleEditViewModel
public bool TreatCollectionsAsShows { get; set; }
public bool ShuffleScheduleItems { get; set; }
public bool RandomStartPoint { get; set; }
public FixedStartTimeBehavior FixedStartTimeBehavior { get; set; }
public UpdateProgramSchedule ToUpdate() =>
new(Id, Name, KeepMultiPartEpisodesTogether, TreatCollectionsAsShows, ShuffleScheduleItems, RandomStartPoint);
new(
Id,
Name,
KeepMultiPartEpisodesTogether,
TreatCollectionsAsShows,
ShuffleScheduleItems,
RandomStartPoint,
FixedStartTimeBehavior);
public CreateProgramSchedule ToCreate() =>
new(Name, KeepMultiPartEpisodesTogether, TreatCollectionsAsShows, ShuffleScheduleItems, RandomStartPoint);
new(
Name,
KeepMultiPartEpisodesTogether,
TreatCollectionsAsShows,
ShuffleScheduleItems,
RandomStartPoint,
FixedStartTimeBehavior);
}

8
ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

@ -6,6 +6,7 @@ using ErsatzTV.Application.MediaCollections; @@ -6,6 +6,7 @@ using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.ViewModels;
@ -18,6 +19,7 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged @@ -18,6 +19,7 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
private int _playoutDurationHours;
private int _playoutDurationMinutes;
private TimeSpan? _startTime;
private FixedStartTimeBehavior? _fixedStartTimeBehavior;
public int Id { get; set; }
public int Index { get; set; }
@ -29,6 +31,12 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged @@ -29,6 +31,12 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
set => _startTime = value;
}
public FixedStartTimeBehavior? FixedStartTimeBehavior
{
get => StartType == StartType.Fixed ? _fixedStartTimeBehavior : null;
set => _fixedStartTimeBehavior = value;
}
public FillWithGroupMode FillWithGroupMode { get; set; }
public bool CanFillWithGroups =>

Loading…
Cancel
Save