Browse Source

add rerun collection type (#2421)

* rename collection type

* split collections into separate pages

* add rerun collection types, migration, editor

* add rerun to classic schedule items

* rerun plumbing in classic playout builder

* start to implement rerun enumerator

* add scheduledAt to enumerator movenext

* maintain rerun history in db

* fix shuffle

* fix rerun allowed playback orders

* fix updating rerun collections

* update changelog; fix editing

* update changelog
pull/2422/head
Jason Dove 4 months ago committed by GitHub
parent
commit
245165c9d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 12
      CHANGELOG.md
  2. 2
      ErsatzTV.Application/Filler/Commands/CreateFillerPreset.cs
  3. 2
      ErsatzTV.Application/Filler/Commands/UpdateFillerPreset.cs
  4. 2
      ErsatzTV.Application/Filler/FillerPresetViewModel.cs
  5. 2
      ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToPlaylistHandler.cs
  6. 22
      ErsatzTV.Application/MediaCollections/Commands/AddItemsToPlaylistHandler.cs
  7. 2
      ErsatzTV.Application/MediaCollections/Commands/AddMovieToPlaylistHandler.cs
  8. 2
      ErsatzTV.Application/MediaCollections/Commands/AddSeasonToPlaylistHandler.cs
  9. 2
      ErsatzTV.Application/MediaCollections/Commands/AddShowToPlaylistHandler.cs
  10. 16
      ErsatzTV.Application/MediaCollections/Commands/CreateRerunCollection.cs
  11. 62
      ErsatzTV.Application/MediaCollections/Commands/CreateRerunCollectionHandler.cs
  12. 5
      ErsatzTV.Application/MediaCollections/Commands/DeleteRerunCollection.cs
  13. 41
      ErsatzTV.Application/MediaCollections/Commands/DeleteRerunCollectionHandler.cs
  14. 2
      ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayoutHandler.cs
  15. 2
      ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItem.cs
  16. 26
      ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItemsHandler.cs
  17. 8
      ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs
  18. 17
      ErsatzTV.Application/MediaCollections/Commands/UpdateRerunCollection.cs
  19. 69
      ErsatzTV.Application/MediaCollections/Commands/UpdateRerunCollectionHandler.cs
  20. 26
      ErsatzTV.Application/MediaCollections/Mapper.cs
  21. 2
      ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs
  22. 3
      ErsatzTV.Application/MediaCollections/PagedRerunCollectionsViewModel.cs
  23. 2
      ErsatzTV.Application/MediaCollections/PlaylistItemViewModel.cs
  24. 3
      ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollections.cs
  25. 26
      ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollectionsHandler.cs
  26. 10
      ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs
  27. 3
      ErsatzTV.Application/MediaCollections/Queries/GetRerunCollectionById.cs
  28. 36
      ErsatzTV.Application/MediaCollections/Queries/GetRerunCollectionByIdHandler.cs
  29. 15
      ErsatzTV.Application/MediaCollections/RerunCollectionViewModel.cs
  30. 16
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  31. 6
      ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs
  32. 3
      ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs
  33. 34
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  34. 3
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs
  35. 4
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs
  36. 12
      ErsatzTV.Application/ProgramSchedules/Mapper.cs
  37. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs
  38. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs
  39. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs
  40. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs
  41. 19
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs
  42. 7
      ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs
  43. 2
      ErsatzTV.Application/Scheduling/BlockItemViewModel.cs
  44. 2
      ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItem.cs
  45. 14
      ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs
  46. 4
      ErsatzTV.Application/Scheduling/Commands/UpdateDeco.cs
  47. 12
      ErsatzTV.Application/Scheduling/Commands/UpdateDecoHandler.cs
  48. 4
      ErsatzTV.Application/Scheduling/DecoViewModel.cs
  49. 5
      ErsatzTV.Application/Search/Queries/SearchRerunCollections.cs
  50. 26
      ErsatzTV.Application/Search/Queries/SearchRerunCollectionsHandler.cs
  51. 2
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  52. 4
      ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs
  53. 6
      ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutChangeDetectionTests.cs
  54. 6
      ErsatzTV.Core.Tests/Scheduling/ChronologicalContentTests.cs
  55. 6
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ContinuePlayoutTests.cs
  56. 22
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs
  57. 10
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/PlayoutBuilderTestBase.cs
  58. 4
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/RefreshPlayoutTests.cs
  59. 6
      ErsatzTV.Core.Tests/Scheduling/CustomOrderContentTests.cs
  60. 36
      ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs
  61. 6
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs
  62. 6
      ErsatzTV.Core.Tests/Scheduling/RandomizedContentTests.cs
  63. 2
      ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs
  64. 8
      ErsatzTV.Core.Tests/Scheduling/SeasonEpisodeContentTests.cs
  65. 10
      ErsatzTV.Core.Tests/Scheduling/ShuffledContentTests.cs
  66. 2
      ErsatzTV.Core/Domain/Collection/PlaylistItem.cs
  67. 21
      ErsatzTV.Core/Domain/Collection/RerunCollection.cs
  68. 4
      ErsatzTV.Core/Domain/CollectionType.cs
  69. 1
      ErsatzTV.Core/Domain/ConfigElementKey.cs
  70. 2
      ErsatzTV.Core/Domain/Filler/FillerPreset.cs
  71. 4
      ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs
  72. 4
      ErsatzTV.Core/Domain/ProgramScheduleItem.cs
  73. 2
      ErsatzTV.Core/Domain/Scheduling/BlockItem.cs
  74. 4
      ErsatzTV.Core/Domain/Scheduling/Deco.cs
  75. 13
      ErsatzTV.Core/Domain/Scheduling/RerunHistory.cs
  76. 2
      ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs
  77. 2
      ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs
  78. 35
      ErsatzTV.Core/Interfaces/Scheduling/IRerunHelper.cs
  79. 2
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs
  80. 14
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs
  81. 2
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutShuffledMediaCollectionEnumerator.cs
  82. 5
      ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs
  83. 125
      ErsatzTV.Core/Scheduling/CollectionKey.cs
  84. 5
      ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs
  85. 20
      ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs
  86. 2
      ErsatzTV.Core/Scheduling/Engine/PlaylistHelper.cs
  87. 16
      ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs
  88. 2
      ErsatzTV.Core/Scheduling/HistoryDetails.cs
  89. 34
      ErsatzTV.Core/Scheduling/MediaItemsForCollection.cs
  90. 6
      ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs
  91. 4
      ErsatzTV.Core/Scheduling/PlayoutBuildResult.cs
  92. 41
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  93. 22
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs
  94. 6
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs
  95. 2
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs
  96. 2
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs
  97. 2
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs
  98. 6
      ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs
  99. 6
      ErsatzTV.Core/Scheduling/RandomizedRotatingMediaCollectionEnumerator.cs
  100. 4
      ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs
  101. Some files were not shown because too many files have changed in this diff Show More

12
CHANGELOG.md

@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added
- Add new collection type `Rerun Collection`
- This collection type will show up as *two* collection types in classic schedules
- `Rerun (First Run)`
- `Rerun (Rerun)`
- The playback order for each of these collection types can be set on the rerun collection itself
- e.g. `Season, Episode` order for first run, `Shuffle` for rerun
- When a first run item is added to a playout, it will immediately be made available in the rerun collection
- Rerun history is currently scoped to the playout, and only supported in classic schedules
- This means resetting the playout will reset the rerun history
- Items will still be scheduled from the rerun collection if it is used before the first run collection
- Otherwise, the rerun collection would be considered "empty" which prevents the playout build altogether
## [25.6.0] - 2025-09-14 ## [25.6.0] - 2025-09-14
### Added ### Added

2
ErsatzTV.Application/Filler/Commands/CreateFillerPreset.cs

@ -12,7 +12,7 @@ public record CreateFillerPreset(
int? Count, int? Count,
int? PadToNearestMinute, int? PadToNearestMinute,
bool AllowWatermarks, bool AllowWatermarks,
ProgramScheduleItemCollectionType CollectionType, CollectionType CollectionType,
int? CollectionId, int? CollectionId,
int? MediaItemId, int? MediaItemId,
int? MultiCollectionId, int? MultiCollectionId,

2
ErsatzTV.Application/Filler/Commands/UpdateFillerPreset.cs

@ -13,7 +13,7 @@ public record UpdateFillerPreset(
int? Count, int? Count,
int? PadToNearestMinute, int? PadToNearestMinute,
bool AllowWatermarks, bool AllowWatermarks,
ProgramScheduleItemCollectionType CollectionType, CollectionType CollectionType,
int? CollectionId, int? CollectionId,
int? MediaItemId, int? MediaItemId,
int? MultiCollectionId, int? MultiCollectionId,

2
ErsatzTV.Application/Filler/FillerPresetViewModel.cs

@ -13,7 +13,7 @@ public record FillerPresetViewModel(
int? Count, int? Count,
int? PadToNearestMinute, int? PadToNearestMinute,
bool AllowWatermarks, bool AllowWatermarks,
ProgramScheduleItemCollectionType CollectionType, CollectionType CollectionType,
int? CollectionId, int? CollectionId,
int? MediaItemId, int? MediaItemId,
int? MultiCollectionId, int? MultiCollectionId,

2
ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToPlaylistHandler.cs

@ -25,7 +25,7 @@ public class AddEpisodeToPlaylistHandler(IDbContextFactory<TvContext> dbContextF
var playlistItem = new PlaylistItem var playlistItem = new PlaylistItem
{ {
Index = index, Index = index,
CollectionType = ProgramScheduleItemCollectionType.Episode, CollectionType = CollectionType.Episode,
MediaItemId = parameters.Episode.Id, MediaItemId = parameters.Episode.Id,
PlaybackOrder = PlaybackOrder.Shuffle, PlaybackOrder = PlaybackOrder.Shuffle,
IncludeInProgramGuide = true IncludeInProgramGuide = true

22
ErsatzTV.Application/MediaCollections/Commands/AddItemsToPlaylistHandler.cs

@ -36,22 +36,22 @@ public class AddItemsToPlaylistHandler : IRequestHandler<AddItemsToPlaylist, Eit
AddItemsToPlaylist request, AddItemsToPlaylist request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var allItems = new Dictionary<ProgramScheduleItemCollectionType, List<int>> var allItems = new Dictionary<CollectionType, List<int>>
{ {
{ ProgramScheduleItemCollectionType.Movie, request.MovieIds }, { CollectionType.Movie, request.MovieIds },
{ ProgramScheduleItemCollectionType.TelevisionShow, request.ShowIds }, { CollectionType.TelevisionShow, request.ShowIds },
{ ProgramScheduleItemCollectionType.TelevisionSeason, request.SeasonIds }, { CollectionType.TelevisionSeason, request.SeasonIds },
{ ProgramScheduleItemCollectionType.Episode, request.EpisodeIds }, { CollectionType.Episode, request.EpisodeIds },
{ ProgramScheduleItemCollectionType.Artist, request.ArtistIds }, { CollectionType.Artist, request.ArtistIds },
{ ProgramScheduleItemCollectionType.MusicVideo, request.MusicVideoIds }, { CollectionType.MusicVideo, request.MusicVideoIds },
{ ProgramScheduleItemCollectionType.OtherVideo, request.OtherVideoIds }, { CollectionType.OtherVideo, request.OtherVideoIds },
{ ProgramScheduleItemCollectionType.Song, request.SongIds }, { CollectionType.Song, request.SongIds },
{ ProgramScheduleItemCollectionType.Image, request.ImageIds } { CollectionType.Image, request.ImageIds }
}; };
int index = playlist.Items.Count > 0 ? playlist.Items.Max(i => i.Index) + 1 : 0; int index = playlist.Items.Count > 0 ? playlist.Items.Max(i => i.Index) + 1 : 0;
foreach ((ProgramScheduleItemCollectionType collectionType, List<int> ids) in allItems) foreach ((CollectionType collectionType, List<int> ids) in allItems)
{ {
foreach (int id in ids) foreach (int id in ids)
{ {

2
ErsatzTV.Application/MediaCollections/Commands/AddMovieToPlaylistHandler.cs

@ -25,7 +25,7 @@ public class AddMovieToPlaylistHandler(IDbContextFactory<TvContext> dbContextFac
var playlistItem = new PlaylistItem var playlistItem = new PlaylistItem
{ {
Index = index, Index = index,
CollectionType = ProgramScheduleItemCollectionType.Movie, CollectionType = CollectionType.Movie,
MediaItemId = parameters.Movie.Id, MediaItemId = parameters.Movie.Id,
PlaybackOrder = PlaybackOrder.Shuffle, PlaybackOrder = PlaybackOrder.Shuffle,
IncludeInProgramGuide = true IncludeInProgramGuide = true

2
ErsatzTV.Application/MediaCollections/Commands/AddSeasonToPlaylistHandler.cs

@ -25,7 +25,7 @@ public class AddSeasonToPlaylistHandler(IDbContextFactory<TvContext> dbContextFa
var playlistItem = new PlaylistItem var playlistItem = new PlaylistItem
{ {
Index = index, Index = index,
CollectionType = ProgramScheduleItemCollectionType.TelevisionSeason, CollectionType = CollectionType.TelevisionSeason,
MediaItemId = parameters.Season.Id, MediaItemId = parameters.Season.Id,
PlaybackOrder = PlaybackOrder.Shuffle, PlaybackOrder = PlaybackOrder.Shuffle,
IncludeInProgramGuide = true IncludeInProgramGuide = true

2
ErsatzTV.Application/MediaCollections/Commands/AddShowToPlaylistHandler.cs

@ -25,7 +25,7 @@ public class AddShowToPlaylistHandler(IDbContextFactory<TvContext> dbContextFact
var playlistItem = new PlaylistItem var playlistItem = new PlaylistItem
{ {
Index = index, Index = index,
CollectionType = ProgramScheduleItemCollectionType.TelevisionShow, CollectionType = CollectionType.TelevisionShow,
MediaItemId = parameters.Show.Id, MediaItemId = parameters.Show.Id,
PlaybackOrder = PlaybackOrder.Shuffle, PlaybackOrder = PlaybackOrder.Shuffle,
IncludeInProgramGuide = true IncludeInProgramGuide = true

16
ErsatzTV.Application/MediaCollections/Commands/CreateRerunCollection.cs

@ -0,0 +1,16 @@
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaCollections;
public record CreateRerunCollection(
string Name,
CollectionType CollectionType,
MediaCollectionViewModel Collection,
MultiCollectionViewModel MultiCollection,
SmartCollectionViewModel SmartCollection,
NamedMediaItemViewModel MediaItem,
PlaybackOrder FirstRunPlaybackOrder,
PlaybackOrder RerunPlaybackOrder)
: IRequest<Either<BaseError, RerunCollectionViewModel>>;

62
ErsatzTV.Application/MediaCollections/Commands/CreateRerunCollectionHandler.cs

@ -0,0 +1,62 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCollections.Mapper;
namespace ErsatzTV.Application.MediaCollections;
public class CreateRerunCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) :
IRequestHandler<CreateRerunCollection, Either<BaseError, RerunCollectionViewModel>>
{
public async Task<Either<BaseError, RerunCollectionViewModel>> Handle(
CreateRerunCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, RerunCollection> validation = await Validate(dbContext, request);
return await validation.Apply(c => PersistCollection(dbContext, c));
}
private static async Task<RerunCollectionViewModel> PersistCollection(
TvContext dbContext,
RerunCollection collection)
{
await dbContext.RerunCollections.AddAsync(collection);
await dbContext.SaveChangesAsync();
return ProjectToViewModel(collection);
}
private static Task<Validation<BaseError, RerunCollection>> Validate(
TvContext dbContext,
CreateRerunCollection request) =>
ValidateName(dbContext, request).MapT(name => new RerunCollection
{
Name = name,
CollectionType = request.CollectionType,
CollectionId = request.Collection?.Id,
MultiCollectionId = request.MultiCollection?.Id,
SmartCollectionId = request.SmartCollection?.Id,
MediaItemId = request.MediaItem?.MediaItemId,
FirstRunPlaybackOrder = request.FirstRunPlaybackOrder,
RerunPlaybackOrder = request.RerunPlaybackOrder
});
private static async Task<Validation<BaseError, string>> ValidateName(
TvContext dbContext,
CreateRerunCollection createCollection)
{
List<string> allNames = await dbContext.RerunCollections
.Map(c => c.Name)
.ToListAsync();
Validation<BaseError, string> result1 = createCollection.NotEmpty(c => c.Name)
.Bind(_ => createCollection.NotLongerThan(50)(c => c.Name));
var result2 = Optional(createCollection.Name)
.Where(name => !allNames.Contains(name))
.ToValidation<BaseError>("Rerun collection name must be unique");
return (result1, result2).Apply((_, _) => createCollection.Name);
}
}

5
ErsatzTV.Application/MediaCollections/Commands/DeleteRerunCollection.cs

@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.MediaCollections;
public record DeleteRerunCollection(int RerunCollectionId) : IRequest<Either<BaseError, Unit>>;

41
ErsatzTV.Application/MediaCollections/Commands/DeleteRerunCollectionHandler.cs

@ -0,0 +1,41 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections;
public class DeleteRerunCollectionHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<DeleteRerunCollection, Either<BaseError, Unit>>
{
public async Task<Either<BaseError, Unit>> Handle(
DeleteRerunCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, RerunCollection> validation = await RerunCollectionMustExist(
dbContext,
request,
cancellationToken);
return await validation.Apply(c => DoDeletion(dbContext, c, cancellationToken));
}
private static async Task<Unit> DoDeletion(
TvContext dbContext,
RerunCollection rerunCollection,
CancellationToken cancellationToken)
{
dbContext.RerunCollections.Remove(rerunCollection);
await dbContext.SaveChangesAsync(cancellationToken);
return Unit.Default;
}
private static Task<Validation<BaseError, RerunCollection>> RerunCollectionMustExist(
TvContext dbContext,
DeleteRerunCollection request,
CancellationToken cancellationToken) =>
dbContext.RerunCollections
.SelectOneAsync(c => c.Id, c => c.Id == request.RerunCollectionId, cancellationToken)
.Map(o => o.ToValidation<BaseError>($"Rerun collection {request.RerunCollectionId} does not exist."));
}

2
ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayoutHandler.cs

@ -120,7 +120,7 @@ public class PreviewPlaylistPlayoutHandler(
private static ProgramScheduleItemFlood MapToScheduleItem(PreviewPlaylistPlayout request) => private static ProgramScheduleItemFlood MapToScheduleItem(PreviewPlaylistPlayout request) =>
new() new()
{ {
CollectionType = ProgramScheduleItemCollectionType.Playlist, CollectionType = CollectionType.Playlist,
Playlist = new Playlist Playlist = new Playlist
{ {
Items = request.Data.Items.OrderBy(i => i.Index).Map(MapToPlaylistItem).ToList() Items = request.Data.Items.OrderBy(i => i.Index).Map(MapToPlaylistItem).ToList()

2
ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItem.cs

@ -4,7 +4,7 @@ namespace ErsatzTV.Application.MediaCollections;
public record ReplacePlaylistItem( public record ReplacePlaylistItem(
int Index, int Index,
ProgramScheduleItemCollectionType CollectionType, CollectionType CollectionType,
int? CollectionId, int? CollectionId,
int? MultiCollectionId, int? MultiCollectionId,
int? SmartCollectionId, int? SmartCollectionId,

26
ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItemsHandler.cs

@ -78,61 +78,61 @@ public class ReplacePlaylistItemsHandler(IDbContextFactory<TvContext> dbContextF
{ {
switch (item.CollectionType) switch (item.CollectionType)
{ {
case ProgramScheduleItemCollectionType.Collection: case CollectionType.Collection:
if (item.CollectionId is null) if (item.CollectionId is null)
{ {
return BaseError.New("[Collection] is required for collection type 'Collection'"); return BaseError.New("[Collection] is required for collection type 'Collection'");
} }
break; break;
case ProgramScheduleItemCollectionType.TelevisionShow: case CollectionType.TelevisionShow:
if (item.MediaItemId is null) if (item.MediaItemId is null)
{ {
return BaseError.New("[MediaItem] is required for collection type 'TelevisionShow'"); return BaseError.New("[MediaItem] is required for collection type 'TelevisionShow'");
} }
break; break;
case ProgramScheduleItemCollectionType.TelevisionSeason: case CollectionType.TelevisionSeason:
if (item.MediaItemId is null) if (item.MediaItemId is null)
{ {
return BaseError.New("[MediaItem] is required for collection type 'TelevisionSeason'"); return BaseError.New("[MediaItem] is required for collection type 'TelevisionSeason'");
} }
break; break;
case ProgramScheduleItemCollectionType.Artist: case CollectionType.Artist:
if (item.MediaItemId is null) if (item.MediaItemId is null)
{ {
return BaseError.New("[MediaItem] is required for collection type 'Artist'"); return BaseError.New("[MediaItem] is required for collection type 'Artist'");
} }
break; break;
case ProgramScheduleItemCollectionType.MultiCollection: case CollectionType.MultiCollection:
if (item.MultiCollectionId is null) if (item.MultiCollectionId is null)
{ {
return BaseError.New("[MultiCollection] is required for collection type 'MultiCollection'"); return BaseError.New("[MultiCollection] is required for collection type 'MultiCollection'");
} }
break; break;
case ProgramScheduleItemCollectionType.SmartCollection: case CollectionType.SmartCollection:
if (item.SmartCollectionId is null) if (item.SmartCollectionId is null)
{ {
return BaseError.New("[SmartCollection] is required for collection type 'SmartCollection'"); return BaseError.New("[SmartCollection] is required for collection type 'SmartCollection'");
} }
break; break;
case ProgramScheduleItemCollectionType.Movie: case CollectionType.Movie:
case ProgramScheduleItemCollectionType.Episode: case CollectionType.Episode:
case ProgramScheduleItemCollectionType.MusicVideo: case CollectionType.MusicVideo:
case ProgramScheduleItemCollectionType.OtherVideo: case CollectionType.OtherVideo:
case ProgramScheduleItemCollectionType.Song: case CollectionType.Song:
case ProgramScheduleItemCollectionType.Image: case CollectionType.Image:
if (item.MediaItemId is null) if (item.MediaItemId is null)
{ {
return BaseError.New($"[MediaItem] is required for type '{item.CollectionType}'"); return BaseError.New($"[MediaItem] is required for type '{item.CollectionType}'");
} }
break; break;
case ProgramScheduleItemCollectionType.FakeCollection: case CollectionType.FakeCollection:
default: default:
return BaseError.New("[CollectionType] is invalid"); return BaseError.New("[CollectionType] is invalid");
} }

8
ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs

@ -203,10 +203,10 @@ public abstract class TraktCommandBase
playlistItem.CollectionType = item.Kind switch playlistItem.CollectionType = item.Kind switch
{ {
TraktListItemKind.Movie => ProgramScheduleItemCollectionType.Movie, TraktListItemKind.Movie => CollectionType.Movie,
TraktListItemKind.Show => ProgramScheduleItemCollectionType.TelevisionShow, TraktListItemKind.Show => CollectionType.TelevisionShow,
TraktListItemKind.Season => ProgramScheduleItemCollectionType.TelevisionSeason, TraktListItemKind.Season => CollectionType.TelevisionSeason,
_ => ProgramScheduleItemCollectionType.Episode _ => CollectionType.Episode
}; };
playlistItem.MediaItemId = item.MediaItemId; playlistItem.MediaItemId = item.MediaItemId;

17
ErsatzTV.Application/MediaCollections/Commands/UpdateRerunCollection.cs

@ -0,0 +1,17 @@
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaCollections;
public record UpdateRerunCollection(
int RerunCollectionId,
string Name,
CollectionType CollectionType,
MediaCollectionViewModel Collection,
MultiCollectionViewModel MultiCollection,
SmartCollectionViewModel SmartCollection,
NamedMediaItemViewModel MediaItem,
PlaybackOrder FirstRunPlaybackOrder,
PlaybackOrder RerunPlaybackOrder)
: IRequest<Either<BaseError, Unit>>;

69
ErsatzTV.Application/MediaCollections/Commands/UpdateRerunCollectionHandler.cs

@ -0,0 +1,69 @@
using System.Threading.Channels;
using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections;
public class UpdateRerunCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
: IRequestHandler<UpdateRerunCollection, Either<BaseError, Unit>>
{
public async Task<Either<BaseError, Unit>> Handle(
UpdateRerunCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, RerunCollection> validation = await Validate(dbContext, request, cancellationToken);
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request, cancellationToken));
}
private async Task<Unit> ApplyUpdateRequest(
TvContext dbContext,
RerunCollection c,
UpdateRerunCollection request,
CancellationToken cancellationToken)
{
c.Name = request.Name;
c.CollectionType = request.CollectionType;
c.CollectionId = request.Collection?.Id;
c.MultiCollectionId = request.MultiCollection?.Id;
c.SmartCollectionId = request.SmartCollection?.Id;
c.MediaItemId = request.MediaItem?.MediaItemId;
c.FirstRunPlaybackOrder = request.FirstRunPlaybackOrder;
c.RerunPlaybackOrder = request.RerunPlaybackOrder;
// rebuild playouts
if (await dbContext.SaveChangesAsync(cancellationToken) > 0)
{
// refresh all playouts that use this rerun collection
foreach (int playoutId in await mediaCollectionRepository.PlayoutIdsUsingRerunCollection(
request.RerunCollectionId))
{
await channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh), cancellationToken);
}
}
return Unit.Default;
}
private static Task<Validation<BaseError, RerunCollection>> Validate(
TvContext dbContext,
UpdateRerunCollection request,
CancellationToken cancellationToken) => RerunCollectionMustExist(dbContext, request, cancellationToken);
private static Task<Validation<BaseError, RerunCollection>> RerunCollectionMustExist(
TvContext dbContext,
UpdateRerunCollection updateCollection,
CancellationToken cancellationToken) =>
dbContext.RerunCollections
.SelectOneAsync(c => c.Id, c => c.Id == updateCollection.RerunCollectionId, cancellationToken)
.Map(o => o.ToValidation<BaseError>("Rerun collection does not exist."));
}

26
ErsatzTV.Application/MediaCollections/Mapper.cs

@ -7,7 +7,7 @@ internal static class Mapper
{ {
internal static MediaCollectionViewModel ProjectToViewModel(Collection collection) => internal static MediaCollectionViewModel ProjectToViewModel(Collection collection) =>
new( new(
ProgramScheduleItemCollectionType.Collection, CollectionType.Collection,
collection.Id, collection.Id,
collection.Name, collection.Name,
collection.UseCustomPlaybackOrder, collection.UseCustomPlaybackOrder,
@ -23,6 +23,30 @@ internal static class Mapper
internal static SmartCollectionViewModel ProjectToViewModel(SmartCollection collection) => internal static SmartCollectionViewModel ProjectToViewModel(SmartCollection collection) =>
new(collection.Id, collection.Name, collection.Query); new(collection.Id, collection.Name, collection.Query);
internal static RerunCollectionViewModel ProjectToViewModel(RerunCollection collection) =>
new(
collection.Id,
collection.Name,
collection.CollectionType,
collection.Collection is not null ? ProjectToViewModel(collection.Collection) : null,
collection.MultiCollection is not null ? ProjectToViewModel(collection.MultiCollection) : null,
collection.SmartCollection is not null ? ProjectToViewModel(collection.SmartCollection) : null,
collection.MediaItem switch
{
Show show => MediaItems.Mapper.ProjectToViewModel(show),
Season season => MediaItems.Mapper.ProjectToViewModel(season),
Artist artist => MediaItems.Mapper.ProjectToViewModel(artist),
Movie movie => MediaItems.Mapper.ProjectToViewModel(movie),
Episode episode => MediaItems.Mapper.ProjectToViewModel(episode),
MusicVideo musicVideo => MediaItems.Mapper.ProjectToViewModel(musicVideo),
OtherVideo otherVideo => MediaItems.Mapper.ProjectToViewModel(otherVideo),
Song song => MediaItems.Mapper.ProjectToViewModel(song),
Image image => MediaItems.Mapper.ProjectToViewModel(image),
_ => null
},
collection.FirstRunPlaybackOrder,
collection.RerunPlaybackOrder);
internal static TraktListViewModel ProjectToViewModel(TraktList traktList) => internal static TraktListViewModel ProjectToViewModel(TraktList traktList) =>
new( new(
traktList.Id, traktList.Id,

2
ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs

@ -4,7 +4,7 @@ using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaCollections; namespace ErsatzTV.Application.MediaCollections;
public record MediaCollectionViewModel( public record MediaCollectionViewModel(
ProgramScheduleItemCollectionType CollectionType, CollectionType CollectionType,
int Id, int Id,
string Name, string Name,
bool UseCustomPlaybackOrder, bool UseCustomPlaybackOrder,

3
ErsatzTV.Application/MediaCollections/PagedRerunCollectionsViewModel.cs

@ -0,0 +1,3 @@
namespace ErsatzTV.Application.MediaCollections;
public record PagedRerunCollectionsViewModel(int TotalCount, List<RerunCollectionViewModel> Page);

2
ErsatzTV.Application/MediaCollections/PlaylistItemViewModel.cs

@ -6,7 +6,7 @@ namespace ErsatzTV.Application.MediaCollections;
public record PlaylistItemViewModel( public record PlaylistItemViewModel(
int Id, int Id,
int Index, int Index,
ProgramScheduleItemCollectionType CollectionType, CollectionType CollectionType,
MediaCollectionViewModel Collection, MediaCollectionViewModel Collection,
MultiCollectionViewModel MultiCollection, MultiCollectionViewModel MultiCollection,
SmartCollectionViewModel SmartCollection, SmartCollectionViewModel SmartCollection,

3
ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollections.cs

@ -0,0 +1,3 @@
namespace ErsatzTV.Application.MediaCollections;
public record GetPagedRerunCollections(int PageNum, int PageSize) : IRequest<PagedRerunCollectionsViewModel>;

26
ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollectionsHandler.cs

@ -0,0 +1,26 @@
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCollections.Mapper;
namespace ErsatzTV.Application.MediaCollections;
public class GetPagedRerunCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetPagedRerunCollections, PagedRerunCollectionsViewModel>
{
public async Task<PagedRerunCollectionsViewModel> Handle(
GetPagedRerunCollections request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
int count = await dbContext.RerunCollections.CountAsync(cancellationToken);
List<RerunCollectionViewModel> page = await dbContext.RerunCollections
.AsNoTracking()
.OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation))
.Skip(request.PageNum * request.PageSize)
.Take(request.PageSize)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
return new PagedRerunCollectionsViewModel(count, page);
}
}

10
ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs

@ -4,18 +4,14 @@ using static ErsatzTV.Application.MediaCollections.Mapper;
namespace ErsatzTV.Application.MediaCollections; namespace ErsatzTV.Application.MediaCollections;
public class GetPagedSmartCollectionsHandler : IRequestHandler<GetPagedSmartCollections, PagedSmartCollectionsViewModel> public class GetPagedSmartCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetPagedSmartCollections, PagedSmartCollectionsViewModel>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetPagedSmartCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<PagedSmartCollectionsViewModel> Handle( public async Task<PagedSmartCollectionsViewModel> Handle(
GetPagedSmartCollections request, GetPagedSmartCollections request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
int count = await dbContext.SmartCollections.CountAsync(cancellationToken); int count = await dbContext.SmartCollections.CountAsync(cancellationToken);
List<SmartCollectionViewModel> page = await dbContext.SmartCollections List<SmartCollectionViewModel> page = await dbContext.SmartCollections
.AsNoTracking() .AsNoTracking()

3
ErsatzTV.Application/MediaCollections/Queries/GetRerunCollectionById.cs

@ -0,0 +1,3 @@
namespace ErsatzTV.Application.MediaCollections;
public record GetRerunCollectionById(int Id) : IRequest<Option<RerunCollectionViewModel>>;

36
ErsatzTV.Application/MediaCollections/Queries/GetRerunCollectionByIdHandler.cs

@ -0,0 +1,36 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCollections.Mapper;
namespace ErsatzTV.Application.MediaCollections;
public class GetRerunCollectionByIdHandler(IDbContextFactory<TvContext> dbContextFactory) :
IRequestHandler<GetRerunCollectionById, Option<RerunCollectionViewModel>>
{
public async Task<Option<RerunCollectionViewModel>> Handle(
GetRerunCollectionById request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.RerunCollections
.AsNoTracking()
.Include(c => c.Collection)
.Include(c => c.MultiCollection)
.Include(c => c.SmartCollection)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Movie).MovieMetadata)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Season).SeasonMetadata)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Season).Show)
.ThenInclude(s => s.ShowMetadata)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Show).ShowMetadata)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Artist).ArtistMetadata)
.SelectOneAsync(c => c.Id, c => c.Id == request.Id, cancellationToken)
.MapT(ProjectToViewModel);
}
}

15
ErsatzTV.Application/MediaCollections/RerunCollectionViewModel.cs

@ -0,0 +1,15 @@
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaCollections;
public record RerunCollectionViewModel(
int Id,
string Name,
CollectionType CollectionType,
MediaCollectionViewModel Collection,
MultiCollectionViewModel MultiCollection,
SmartCollectionViewModel SmartCollection,
NamedMediaItemViewModel MediaItem,
PlaybackOrder FirstRunPlaybackOrder,
PlaybackOrder RerunPlaybackOrder);

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

@ -28,6 +28,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
private readonly IFFmpegSegmenterService _ffmpegSegmenterService; private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IPlayoutBuilder _playoutBuilder; private readonly IPlayoutBuilder _playoutBuilder;
private readonly IPlayoutTimeShifter _playoutTimeShifter; private readonly IPlayoutTimeShifter _playoutTimeShifter;
private readonly IRerunHelper _rerunHelper;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel; private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly ISequentialPlayoutBuilder _sequentialPlayoutBuilder; private readonly ISequentialPlayoutBuilder _sequentialPlayoutBuilder;
private readonly IScriptedPlayoutBuilder _scriptedPlayoutBuilder; private readonly IScriptedPlayoutBuilder _scriptedPlayoutBuilder;
@ -44,6 +45,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
IFFmpegSegmenterService ffmpegSegmenterService, IFFmpegSegmenterService ffmpegSegmenterService,
IEntityLocker entityLocker, IEntityLocker entityLocker,
IPlayoutTimeShifter playoutTimeShifter, IPlayoutTimeShifter playoutTimeShifter,
IRerunHelper rerunHelper,
ChannelWriter<IBackgroundServiceRequest> workerChannel) ChannelWriter<IBackgroundServiceRequest> workerChannel)
{ {
_client = client; _client = client;
@ -57,6 +59,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
_ffmpegSegmenterService = ffmpegSegmenterService; _ffmpegSegmenterService = ffmpegSegmenterService;
_entityLocker = entityLocker; _entityLocker = entityLocker;
_playoutTimeShifter = playoutTimeShifter; _playoutTimeShifter = playoutTimeShifter;
_rerunHelper = rerunHelper;
_workerChannel = workerChannel; _workerChannel = workerChannel;
} }
@ -160,6 +163,19 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
var changeCount = 0; var changeCount = 0;
if (result.RerunHistoryToRemove.Count > 0)
{
changeCount += await dbContext.RerunHistory
.Where(rh => result.RerunHistoryToRemove.Contains(rh.Id))
.ExecuteDeleteAsync(cancellationToken);
}
if (result.AddedRerunHistory.Count > 0)
{
changeCount += 1;
await dbContext.BulkInsertAsync(result.AddedRerunHistory, cancellationToken: cancellationToken);
}
if (result.ClearItems) if (result.ClearItems)
{ {
changeCount += await dbContext.PlayoutItems changeCount += await dbContext.PlayoutItems

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

@ -10,10 +10,11 @@ public record AddProgramScheduleItem(
TimeSpan? StartTime, TimeSpan? StartTime,
FixedStartTimeBehavior? FixedStartTimeBehavior, FixedStartTimeBehavior? FixedStartTimeBehavior,
PlayoutMode PlayoutMode, PlayoutMode PlayoutMode,
ProgramScheduleItemCollectionType CollectionType, CollectionType CollectionType,
int? CollectionId, int? CollectionId,
int? MultiCollectionId, int? MultiCollectionId,
int? SmartCollectionId, int? SmartCollectionId,
int? RerunCollectionId,
int? MediaItemId, int? MediaItemId,
int? PlaylistId, int? PlaylistId,
PlaybackOrder PlaybackOrder, PlaybackOrder PlaybackOrder,
@ -42,7 +43,7 @@ public record AddProgramScheduleItem(
ChannelSubtitleMode? SubtitleMode) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, ChannelSubtitleMode? SubtitleMode) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>,
IProgramScheduleItemRequest IProgramScheduleItemRequest
{ {
public static AddProgramScheduleItem ForMediaItem(int scheduleId, ProgramScheduleItemCollectionType collectionType, int mediaItemId) public static AddProgramScheduleItem ForMediaItem(int scheduleId, CollectionType collectionType, int mediaItemId)
=> new( => new(
scheduleId, scheduleId,
StartType.Dynamic, StartType.Dynamic,
@ -53,6 +54,7 @@ public record AddProgramScheduleItem(
CollectionId: null, CollectionId: null,
MultiCollectionId: null, MultiCollectionId: null,
SmartCollectionId: null, SmartCollectionId: null,
RerunCollectionId: null,
MediaItemId: mediaItemId, MediaItemId: mediaItemId,
PlaylistId: null, PlaylistId: null,
PlaybackOrder.Shuffle, PlaybackOrder.Shuffle,

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

@ -7,10 +7,11 @@ public interface IProgramScheduleItemRequest
{ {
TimeSpan? StartTime { get; } TimeSpan? StartTime { get; }
FixedStartTimeBehavior? FixedStartTimeBehavior { get; } FixedStartTimeBehavior? FixedStartTimeBehavior { get; }
ProgramScheduleItemCollectionType CollectionType { get; } CollectionType CollectionType { get; }
int? CollectionId { get; } int? CollectionId { get; }
int? MultiCollectionId { get; } int? MultiCollectionId { get; }
int? SmartCollectionId { get; } int? SmartCollectionId { get; }
int? RerunCollectionId { get; }
int? MediaItemId { get; } int? MediaItemId { get; }
int? PlaylistId { get; } int? PlaylistId { get; }
PlayoutMode PlayoutMode { get; } PlayoutMode PlayoutMode { get; }

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

@ -73,7 +73,7 @@ public abstract class ProgramScheduleItemCommandBase
break; break;
case PlayoutMode.Multiple: case PlayoutMode.Multiple:
if (item.MultipleMode is MultipleMode.PlaylistItemSize && if (item.MultipleMode is MultipleMode.PlaylistItemSize &&
item.CollectionType is not ProgramScheduleItemCollectionType.Playlist) item.CollectionType is not CollectionType.Playlist)
{ {
return BaseError.New( return BaseError.New(
"[MultipleMode] cannot be [PlaylistItemSize] when collection is not a playlist"); "[MultipleMode] cannot be [PlaylistItemSize] when collection is not a playlist");
@ -126,49 +126,63 @@ public abstract class ProgramScheduleItemCommandBase
{ {
switch (item.CollectionType) switch (item.CollectionType)
{ {
case ProgramScheduleItemCollectionType.Collection: case CollectionType.Collection:
if (item.CollectionId is null) if (item.CollectionId is null)
{ {
return BaseError.New("[Collection] is required for collection type 'Collection'"); return BaseError.New("[Collection] is required for collection type 'Collection'");
} }
break; break;
case ProgramScheduleItemCollectionType.TelevisionShow: case CollectionType.TelevisionShow:
if (item.MediaItemId is null) if (item.MediaItemId is null)
{ {
return BaseError.New("[MediaItem] is required for collection type 'TelevisionShow'"); return BaseError.New("[MediaItem] is required for collection type 'TelevisionShow'");
} }
break; break;
case ProgramScheduleItemCollectionType.TelevisionSeason: case CollectionType.TelevisionSeason:
if (item.MediaItemId is null) if (item.MediaItemId is null)
{ {
return BaseError.New("[MediaItem] is required for collection type 'TelevisionSeason'"); return BaseError.New("[MediaItem] is required for collection type 'TelevisionSeason'");
} }
break; break;
case ProgramScheduleItemCollectionType.Artist: case CollectionType.Artist:
if (item.MediaItemId is null) if (item.MediaItemId is null)
{ {
return BaseError.New("[MediaItem] is required for collection type 'Artist'"); return BaseError.New("[MediaItem] is required for collection type 'Artist'");
} }
break; break;
case ProgramScheduleItemCollectionType.MultiCollection: case CollectionType.MultiCollection:
if (item.MultiCollectionId is null) if (item.MultiCollectionId is null)
{ {
return BaseError.New("[MultiCollection] is required for collection type 'MultiCollection'"); return BaseError.New("[MultiCollection] is required for collection type 'MultiCollection'");
} }
break; break;
case ProgramScheduleItemCollectionType.SmartCollection: case CollectionType.SmartCollection:
if (item.SmartCollectionId is null) if (item.SmartCollectionId is null)
{ {
return BaseError.New("[SmartCollection] is required for collection type 'SmartCollection'"); return BaseError.New("[SmartCollection] is required for collection type 'SmartCollection'");
} }
break; break;
case ProgramScheduleItemCollectionType.Playlist: case CollectionType.RerunFirstRun:
if (item.RerunCollectionId is null)
{
return BaseError.New("[RerunCollection] is required for collection type 'RerunFirstRun'");
}
break;
case CollectionType.RerunRerun:
if (item.RerunCollectionId is null)
{
return BaseError.New("[RerunCollection] is required for collection type 'RerunRerun'");
}
break;
case CollectionType.Playlist:
if (item.PlaylistId is null) if (item.PlaylistId is null)
{ {
return BaseError.New("[Playlist] is required for collection type 'Playlist'"); return BaseError.New("[Playlist] is required for collection type 'Playlist'");
@ -199,6 +213,7 @@ public abstract class ProgramScheduleItemCommandBase
CollectionId = item.CollectionId, CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId, MultiCollectionId = item.MultiCollectionId,
SmartCollectionId = item.SmartCollectionId, SmartCollectionId = item.SmartCollectionId,
RerunCollectionId = item.RerunCollectionId,
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
PlaylistId = item.PlaylistId, PlaylistId = item.PlaylistId,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,
@ -229,6 +244,7 @@ public abstract class ProgramScheduleItemCommandBase
CollectionId = item.CollectionId, CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId, MultiCollectionId = item.MultiCollectionId,
SmartCollectionId = item.SmartCollectionId, SmartCollectionId = item.SmartCollectionId,
RerunCollectionId = item.RerunCollectionId,
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
PlaylistId = item.PlaylistId, PlaylistId = item.PlaylistId,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,
@ -259,6 +275,7 @@ public abstract class ProgramScheduleItemCommandBase
CollectionId = item.CollectionId, CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId, MultiCollectionId = item.MultiCollectionId,
SmartCollectionId = item.SmartCollectionId, SmartCollectionId = item.SmartCollectionId,
RerunCollectionId = item.RerunCollectionId,
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
PlaylistId = item.PlaylistId, PlaylistId = item.PlaylistId,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,
@ -291,6 +308,7 @@ public abstract class ProgramScheduleItemCommandBase
CollectionId = item.CollectionId, CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId, MultiCollectionId = item.MultiCollectionId,
SmartCollectionId = item.SmartCollectionId, SmartCollectionId = item.SmartCollectionId,
RerunCollectionId = item.RerunCollectionId,
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
PlaylistId = item.PlaylistId, PlaylistId = item.PlaylistId,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,

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

@ -10,10 +10,11 @@ public record ReplaceProgramScheduleItem(
TimeSpan? StartTime, TimeSpan? StartTime,
FixedStartTimeBehavior? FixedStartTimeBehavior, FixedStartTimeBehavior? FixedStartTimeBehavior,
PlayoutMode PlayoutMode, PlayoutMode PlayoutMode,
ProgramScheduleItemCollectionType CollectionType, CollectionType CollectionType,
int? CollectionId, int? CollectionId,
int? MultiCollectionId, int? MultiCollectionId,
int? SmartCollectionId, int? SmartCollectionId,
int? RerunCollectionId,
int? MediaItemId, int? MediaItemId,
int? PlaylistId, int? PlaylistId,
PlaybackOrder PlaybackOrder, PlaybackOrder PlaybackOrder,

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

@ -120,6 +120,7 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase
item.MediaItemId, item.MediaItemId,
item.MultiCollectionId, item.MultiCollectionId,
item.SmartCollectionId, item.SmartCollectionId,
item.RerunCollectionId,
item.PlaylistId); item.PlaylistId);
if (keyOrders.TryGetValue(key, out System.Collections.Generic.HashSet<PlaybackOrder> playbackOrders)) if (keyOrders.TryGetValue(key, out System.Collections.Generic.HashSet<PlaybackOrder> playbackOrders))
@ -140,10 +141,11 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase
} }
private sealed record CollectionKey( private sealed record CollectionKey(
ProgramScheduleItemCollectionType CollectionType, CollectionType CollectionType,
int? CollectionId, int? CollectionId,
int? MediaItemId, int? MediaItemId,
int? MultiCollectionId, int? MultiCollectionId,
int? SmartCollectionId, int? SmartCollectionId,
int? RerunCollectionId,
int? PlaylistId); int? PlaylistId);
} }

12
ErsatzTV.Application/ProgramSchedules/Mapper.cs

@ -34,6 +34,9 @@ internal static class Mapper
duration.SmartCollection != null duration.SmartCollection != null
? MediaCollections.Mapper.ProjectToViewModel(duration.SmartCollection) ? MediaCollections.Mapper.ProjectToViewModel(duration.SmartCollection)
: null, : null,
duration.RerunCollection != null
? MediaCollections.Mapper.ProjectToViewModel(duration.RerunCollection)
: null,
duration.Playlist != null duration.Playlist != null
? MediaCollections.Mapper.ProjectToViewModel(duration.Playlist) ? MediaCollections.Mapper.ProjectToViewModel(duration.Playlist)
: null, : null,
@ -95,6 +98,9 @@ internal static class Mapper
flood.SmartCollection != null flood.SmartCollection != null
? MediaCollections.Mapper.ProjectToViewModel(flood.SmartCollection) ? MediaCollections.Mapper.ProjectToViewModel(flood.SmartCollection)
: null, : null,
flood.RerunCollection != null
? MediaCollections.Mapper.ProjectToViewModel(flood.RerunCollection)
: null,
flood.Playlist != null flood.Playlist != null
? MediaCollections.Mapper.ProjectToViewModel(flood.Playlist) ? MediaCollections.Mapper.ProjectToViewModel(flood.Playlist)
: null, : null,
@ -153,6 +159,9 @@ internal static class Mapper
multiple.SmartCollection != null multiple.SmartCollection != null
? MediaCollections.Mapper.ProjectToViewModel(multiple.SmartCollection) ? MediaCollections.Mapper.ProjectToViewModel(multiple.SmartCollection)
: null, : null,
multiple.RerunCollection != null
? MediaCollections.Mapper.ProjectToViewModel(multiple.RerunCollection)
: null,
multiple.Playlist != null multiple.Playlist != null
? MediaCollections.Mapper.ProjectToViewModel(multiple.Playlist) ? MediaCollections.Mapper.ProjectToViewModel(multiple.Playlist)
: null, : null,
@ -213,6 +222,9 @@ internal static class Mapper
one.SmartCollection != null one.SmartCollection != null
? MediaCollections.Mapper.ProjectToViewModel(one.SmartCollection) ? MediaCollections.Mapper.ProjectToViewModel(one.SmartCollection)
: null, : null,
one.RerunCollection != null
? MediaCollections.Mapper.ProjectToViewModel(one.RerunCollection)
: null,
one.Playlist != null one.Playlist != null
? MediaCollections.Mapper.ProjectToViewModel(one.Playlist) ? MediaCollections.Mapper.ProjectToViewModel(one.Playlist)
: null, : null,

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs

@ -16,10 +16,11 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
StartType startType, StartType startType,
TimeSpan? startTime, TimeSpan? startTime,
FixedStartTimeBehavior? fixedStartTimeBehavior, FixedStartTimeBehavior? fixedStartTimeBehavior,
ProgramScheduleItemCollectionType collectionType, CollectionType collectionType,
MediaCollectionViewModel collection, MediaCollectionViewModel collection,
MultiCollectionViewModel multiCollection, MultiCollectionViewModel multiCollection,
SmartCollectionViewModel smartCollection, SmartCollectionViewModel smartCollection,
RerunCollectionViewModel rerunCollection,
PlaylistViewModel playlist, PlaylistViewModel playlist,
NamedMediaItemViewModel mediaItem, NamedMediaItemViewModel mediaItem,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,
@ -54,6 +55,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
collection, collection,
multiCollection, multiCollection,
smartCollection, smartCollection,
rerunCollection,
playlist, playlist,
mediaItem, mediaItem,
playbackOrder, playbackOrder,

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs

@ -16,10 +16,11 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
StartType startType, StartType startType,
TimeSpan? startTime, TimeSpan? startTime,
FixedStartTimeBehavior? fixedStartTimeBehavior, FixedStartTimeBehavior? fixedStartTimeBehavior,
ProgramScheduleItemCollectionType collectionType, CollectionType collectionType,
MediaCollectionViewModel collection, MediaCollectionViewModel collection,
MultiCollectionViewModel multiCollection, MultiCollectionViewModel multiCollection,
SmartCollectionViewModel smartCollection, SmartCollectionViewModel smartCollection,
RerunCollectionViewModel rerunCollection,
PlaylistViewModel playlist, PlaylistViewModel playlist,
NamedMediaItemViewModel mediaItem, NamedMediaItemViewModel mediaItem,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,
@ -51,6 +52,7 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
collection, collection,
multiCollection, multiCollection,
smartCollection, smartCollection,
rerunCollection,
playlist, playlist,
mediaItem, mediaItem,
playbackOrder, playbackOrder,

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs

@ -16,10 +16,11 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
StartType startType, StartType startType,
TimeSpan? startTime, TimeSpan? startTime,
FixedStartTimeBehavior? fixedStartTimeBehavior, FixedStartTimeBehavior? fixedStartTimeBehavior,
ProgramScheduleItemCollectionType collectionType, CollectionType collectionType,
MediaCollectionViewModel collection, MediaCollectionViewModel collection,
MultiCollectionViewModel multiCollection, MultiCollectionViewModel multiCollection,
SmartCollectionViewModel smartCollection, SmartCollectionViewModel smartCollection,
RerunCollectionViewModel rerunCollection,
PlaylistViewModel playlist, PlaylistViewModel playlist,
NamedMediaItemViewModel mediaItem, NamedMediaItemViewModel mediaItem,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,
@ -53,6 +54,7 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
collection, collection,
multiCollection, multiCollection,
smartCollection, smartCollection,
rerunCollection,
playlist, playlist,
mediaItem, mediaItem,
playbackOrder, playbackOrder,

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs

@ -16,10 +16,11 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
StartType startType, StartType startType,
TimeSpan? startTime, TimeSpan? startTime,
FixedStartTimeBehavior? fixedStartTimeBehavior, FixedStartTimeBehavior? fixedStartTimeBehavior,
ProgramScheduleItemCollectionType collectionType, CollectionType collectionType,
MediaCollectionViewModel collection, MediaCollectionViewModel collection,
MultiCollectionViewModel multiCollection, MultiCollectionViewModel multiCollection,
SmartCollectionViewModel smartCollection, SmartCollectionViewModel smartCollection,
RerunCollectionViewModel rerunCollection,
PlaylistViewModel playlist, PlaylistViewModel playlist,
NamedMediaItemViewModel mediaItem, NamedMediaItemViewModel mediaItem,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,
@ -51,6 +52,7 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
collection, collection,
multiCollection, multiCollection,
smartCollection, smartCollection,
rerunCollection,
playlist, playlist,
mediaItem, mediaItem,
playbackOrder, playbackOrder,

19
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs

@ -15,10 +15,11 @@ public abstract record ProgramScheduleItemViewModel(
TimeSpan? StartTime, TimeSpan? StartTime,
FixedStartTimeBehavior? FixedStartTimeBehavior, FixedStartTimeBehavior? FixedStartTimeBehavior,
PlayoutMode PlayoutMode, PlayoutMode PlayoutMode,
ProgramScheduleItemCollectionType CollectionType, CollectionType CollectionType,
MediaCollectionViewModel Collection, MediaCollectionViewModel Collection,
MultiCollectionViewModel MultiCollection, MultiCollectionViewModel MultiCollection,
SmartCollectionViewModel SmartCollection, SmartCollectionViewModel SmartCollection,
RerunCollectionViewModel RerunCollection,
PlaylistViewModel Playlist, PlaylistViewModel Playlist,
NamedMediaItemViewModel MediaItem, NamedMediaItemViewModel MediaItem,
PlaybackOrder PlaybackOrder, PlaybackOrder PlaybackOrder,
@ -43,19 +44,21 @@ public abstract record ProgramScheduleItemViewModel(
{ {
public string Name => CollectionType switch public string Name => CollectionType switch
{ {
ProgramScheduleItemCollectionType.Collection => Collection?.Name, CollectionType.Collection => Collection?.Name,
ProgramScheduleItemCollectionType.TelevisionShow => CollectionType.TelevisionShow =>
MediaItem?.Name, // $"{TelevisionShow?.Title} ({TelevisionShow?.Year})", MediaItem?.Name, // $"{TelevisionShow?.Title} ({TelevisionShow?.Year})",
ProgramScheduleItemCollectionType.TelevisionSeason => CollectionType.TelevisionSeason =>
MediaItem?.Name, // $"{TelevisionSeason?.Title} ({TelevisionSeason?.Plot})", MediaItem?.Name, // $"{TelevisionSeason?.Title} ({TelevisionSeason?.Plot})",
ProgramScheduleItemCollectionType.Artist => CollectionType.Artist =>
MediaItem?.Name, MediaItem?.Name,
ProgramScheduleItemCollectionType.MultiCollection => CollectionType.MultiCollection =>
MultiCollection?.Name, MultiCollection?.Name,
ProgramScheduleItemCollectionType.SmartCollection => CollectionType.SmartCollection =>
SmartCollection?.Name, SmartCollection?.Name,
ProgramScheduleItemCollectionType.Playlist => CollectionType.Playlist =>
Playlist?.Name, Playlist?.Name,
CollectionType.RerunFirstRun or CollectionType.RerunRerun =>
RerunCollection?.Name,
_ => string.Empty _ => string.Empty
}; };
} }

7
ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs

@ -23,6 +23,7 @@ public class GetProgramScheduleItemsHandler(IDbContextFactory<TvContext> dbConte
.Include(i => i.Collection) .Include(i => i.Collection)
.Include(i => i.MultiCollection) .Include(i => i.MultiCollection)
.Include(i => i.SmartCollection) .Include(i => i.SmartCollection)
.Include(i => i.RerunCollection)
.Include(i => i.Playlist) .Include(i => i.Playlist)
.Include(i => i.MediaItem) .Include(i => i.MediaItem)
.ThenInclude(i => (i as Movie).MovieMetadata) .ThenInclude(i => (i as Movie).MovieMetadata)
@ -74,6 +75,12 @@ public class GetProgramScheduleItemsHandler(IDbContextFactory<TvContext> dbConte
{ {
item = item with { FillWithGroupMode = FillWithGroupMode.None }; item = item with { FillWithGroupMode = FillWithGroupMode.None };
} }
if (item.CollectionType is CollectionType.Playlist or CollectionType.RerunFirstRun
or CollectionType.RerunRerun)
{
item = item with { PlaybackOrder = PlaybackOrder.None };
}
} }
return item; return item;

2
ErsatzTV.Application/Scheduling/BlockItemViewModel.cs

@ -7,7 +7,7 @@ namespace ErsatzTV.Application.Scheduling;
public record BlockItemViewModel( public record BlockItemViewModel(
int Id, int Id,
int Index, int Index,
ProgramScheduleItemCollectionType CollectionType, CollectionType CollectionType,
MediaCollectionViewModel Collection, MediaCollectionViewModel Collection,
MultiCollectionViewModel MultiCollection, MultiCollectionViewModel MultiCollection,
SmartCollectionViewModel SmartCollection, SmartCollectionViewModel SmartCollection,

2
ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItem.cs

@ -4,7 +4,7 @@ namespace ErsatzTV.Application.Scheduling;
public record ReplaceBlockItem( public record ReplaceBlockItem(
int Index, int Index,
ProgramScheduleItemCollectionType CollectionType, CollectionType CollectionType,
int? CollectionId, int? CollectionId,
int? MultiCollectionId, int? MultiCollectionId,
int? SmartCollectionId, int? SmartCollectionId,

14
ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs

@ -89,49 +89,49 @@ public class ReplaceBlockItemsHandler(IDbContextFactory<TvContext> dbContextFact
{ {
switch (item.CollectionType) switch (item.CollectionType)
{ {
case ProgramScheduleItemCollectionType.Collection: case CollectionType.Collection:
if (item.CollectionId is null) if (item.CollectionId is null)
{ {
return BaseError.New("[Collection] is required for collection type 'Collection'"); return BaseError.New("[Collection] is required for collection type 'Collection'");
} }
break; break;
case ProgramScheduleItemCollectionType.TelevisionShow: case CollectionType.TelevisionShow:
if (item.MediaItemId is null) if (item.MediaItemId is null)
{ {
return BaseError.New("[MediaItem] is required for collection type 'TelevisionShow'"); return BaseError.New("[MediaItem] is required for collection type 'TelevisionShow'");
} }
break; break;
case ProgramScheduleItemCollectionType.TelevisionSeason: case CollectionType.TelevisionSeason:
if (item.MediaItemId is null) if (item.MediaItemId is null)
{ {
return BaseError.New("[MediaItem] is required for collection type 'TelevisionSeason'"); return BaseError.New("[MediaItem] is required for collection type 'TelevisionSeason'");
} }
break; break;
case ProgramScheduleItemCollectionType.Artist: case CollectionType.Artist:
if (item.MediaItemId is null) if (item.MediaItemId is null)
{ {
return BaseError.New("[MediaItem] is required for collection type 'Artist'"); return BaseError.New("[MediaItem] is required for collection type 'Artist'");
} }
break; break;
case ProgramScheduleItemCollectionType.MultiCollection: case CollectionType.MultiCollection:
if (item.MultiCollectionId is null) if (item.MultiCollectionId is null)
{ {
return BaseError.New("[MultiCollection] is required for collection type 'MultiCollection'"); return BaseError.New("[MultiCollection] is required for collection type 'MultiCollection'");
} }
break; break;
case ProgramScheduleItemCollectionType.SmartCollection: case CollectionType.SmartCollection:
if (item.SmartCollectionId is null) if (item.SmartCollectionId is null)
{ {
return BaseError.New("[SmartCollection] is required for collection type 'SmartCollection'"); return BaseError.New("[SmartCollection] is required for collection type 'SmartCollection'");
} }
break; break;
case ProgramScheduleItemCollectionType.FakeCollection: case CollectionType.FakeCollection:
default: default:
return BaseError.New("[CollectionType] is invalid"); return BaseError.New("[CollectionType] is invalid");
} }

4
ErsatzTV.Application/Scheduling/Commands/UpdateDeco.cs

@ -15,14 +15,14 @@ public record UpdateDeco(
List<int> GraphicsElementIds, List<int> GraphicsElementIds,
bool UseGraphicsElementsDuringFiller, bool UseGraphicsElementsDuringFiller,
DecoMode DefaultFillerMode, DecoMode DefaultFillerMode,
ProgramScheduleItemCollectionType DefaultFillerCollectionType, CollectionType DefaultFillerCollectionType,
int? DefaultFillerCollectionId, int? DefaultFillerCollectionId,
int? DefaultFillerMediaItemId, int? DefaultFillerMediaItemId,
int? DefaultFillerMultiCollectionId, int? DefaultFillerMultiCollectionId,
int? DefaultFillerSmartCollectionId, int? DefaultFillerSmartCollectionId,
bool DefaultFillerTrimToFit, bool DefaultFillerTrimToFit,
DecoMode DeadAirFallbackMode, DecoMode DeadAirFallbackMode,
ProgramScheduleItemCollectionType DeadAirFallbackCollectionType, CollectionType DeadAirFallbackCollectionType,
int? DeadAirFallbackCollectionId, int? DeadAirFallbackCollectionId,
int? DeadAirFallbackMediaItemId, int? DeadAirFallbackMediaItemId,
int? DeadAirFallbackMultiCollectionId, int? DeadAirFallbackMultiCollectionId,

12
ErsatzTV.Application/Scheduling/Commands/UpdateDecoHandler.cs

@ -92,13 +92,13 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
{ {
switch (request.DefaultFillerCollectionType) switch (request.DefaultFillerCollectionType)
{ {
case ProgramScheduleItemCollectionType.Collection: case CollectionType.Collection:
existing.DefaultFillerCollectionId = request.DefaultFillerCollectionId; existing.DefaultFillerCollectionId = request.DefaultFillerCollectionId;
break; break;
case ProgramScheduleItemCollectionType.MultiCollection: case CollectionType.MultiCollection:
existing.DefaultFillerMultiCollectionId = request.DefaultFillerMultiCollectionId; existing.DefaultFillerMultiCollectionId = request.DefaultFillerMultiCollectionId;
break; break;
case ProgramScheduleItemCollectionType.SmartCollection: case CollectionType.SmartCollection:
existing.DefaultFillerSmartCollectionId = request.DefaultFillerSmartCollectionId; existing.DefaultFillerSmartCollectionId = request.DefaultFillerSmartCollectionId;
break; break;
default: default:
@ -120,13 +120,13 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
{ {
switch (request.DeadAirFallbackCollectionType) switch (request.DeadAirFallbackCollectionType)
{ {
case ProgramScheduleItemCollectionType.Collection: case CollectionType.Collection:
existing.DeadAirFallbackCollectionId = request.DeadAirFallbackCollectionId; existing.DeadAirFallbackCollectionId = request.DeadAirFallbackCollectionId;
break; break;
case ProgramScheduleItemCollectionType.MultiCollection: case CollectionType.MultiCollection:
existing.DeadAirFallbackMultiCollectionId = request.DeadAirFallbackMultiCollectionId; existing.DeadAirFallbackMultiCollectionId = request.DeadAirFallbackMultiCollectionId;
break; break;
case ProgramScheduleItemCollectionType.SmartCollection: case CollectionType.SmartCollection:
existing.DeadAirFallbackSmartCollectionId = request.DeadAirFallbackSmartCollectionId; existing.DeadAirFallbackSmartCollectionId = request.DeadAirFallbackSmartCollectionId;
break; break;
default: default:

4
ErsatzTV.Application/Scheduling/DecoViewModel.cs

@ -17,14 +17,14 @@ public record DecoViewModel(
List<GraphicsElementViewModel> GraphicsElements, List<GraphicsElementViewModel> GraphicsElements,
bool UseGraphicsElementsDuringFiller, bool UseGraphicsElementsDuringFiller,
DecoMode DefaultFillerMode, DecoMode DefaultFillerMode,
ProgramScheduleItemCollectionType DefaultFillerCollectionType, CollectionType DefaultFillerCollectionType,
int? DefaultFillerCollectionId, int? DefaultFillerCollectionId,
int? DefaultFillerMediaItemId, int? DefaultFillerMediaItemId,
int? DefaultFillerMultiCollectionId, int? DefaultFillerMultiCollectionId,
int? DefaultFillerSmartCollectionId, int? DefaultFillerSmartCollectionId,
bool DefaultFillerTrimToFit, bool DefaultFillerTrimToFit,
DecoMode DeadAirFallbackMode, DecoMode DeadAirFallbackMode,
ProgramScheduleItemCollectionType DeadAirFallbackCollectionType, CollectionType DeadAirFallbackCollectionType,
int? DeadAirFallbackCollectionId, int? DeadAirFallbackCollectionId,
int? DeadAirFallbackMediaItemId, int? DeadAirFallbackMediaItemId,
int? DeadAirFallbackMultiCollectionId, int? DeadAirFallbackMultiCollectionId,

5
ErsatzTV.Application/Search/Queries/SearchRerunCollections.cs

@ -0,0 +1,5 @@
using ErsatzTV.Application.MediaCollections;
namespace ErsatzTV.Application.Search;
public record SearchRerunCollections(string Query) : IRequest<List<RerunCollectionViewModel>>;

26
ErsatzTV.Application/Search/Queries/SearchRerunCollectionsHandler.cs

@ -0,0 +1,26 @@
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCollections.Mapper;
namespace ErsatzTV.Application.Search;
public class SearchRerunCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<SearchRerunCollections, List<RerunCollectionViewModel>>
{
public async Task<List<RerunCollectionViewModel>> Handle(
SearchRerunCollections request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.RerunCollections
.AsNoTracking()
.Where(c => EF.Functions.Like(
EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation),
$"%{request.Query}%"))
.OrderBy(c => EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation))
.Take(10)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}

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

@ -849,7 +849,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
private sealed record DisableDeadAirFallback : DeadAirFallbackResult; private sealed record DisableDeadAirFallback : DeadAirFallbackResult;
private sealed record CustomDeadAirFallback( private sealed record CustomDeadAirFallback(
ProgramScheduleItemCollectionType CollectionType, CollectionType CollectionType,
int? CollectionId, int? CollectionId,
int? MediaItemId, int? MediaItemId,
int? MultiCollectionId, int? MultiCollectionId,

4
ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs

@ -42,6 +42,7 @@ public class FakeMediaCollectionRepository : IMediaCollectionRepository
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
throw new NotSupportedException(); throw new NotSupportedException();
public Task<List<MediaItem>> GetRerunCollectionItems(int id, CancellationToken cancellationToken) => throw new NotSupportedException();
public Task<List<MediaItem>> GetShowItemsByShowGuids(List<string> guids) => throw new NotSupportedException(); public Task<List<MediaItem>> GetShowItemsByShowGuids(List<string> guids) => throw new NotSupportedException();
public Task<List<MediaItem>> GetPlaylistItems(int id, CancellationToken cancellationToken) => throw new NotSupportedException(); public Task<List<MediaItem>> GetPlaylistItems(int id, CancellationToken cancellationToken) => throw new NotSupportedException();
public Task<List<Movie>> GetMovie(int id) => throw new NotSupportedException(); public Task<List<Movie>> GetMovie(int id) => throw new NotSupportedException();
@ -69,6 +70,9 @@ public class FakeMediaCollectionRepository : IMediaCollectionRepository
public Task<List<int>> PlayoutIdsUsingSmartCollection(int smartCollectionId) => public Task<List<int>> PlayoutIdsUsingSmartCollection(int smartCollectionId) =>
throw new NotSupportedException(); throw new NotSupportedException();
public Task<List<int>> PlayoutIdsUsingRerunCollection(int rerunCollectionId) =>
throw new NotSupportedException();
public Task<bool> IsCustomPlaybackOrder(int collectionId) => false.AsTask(); public Task<bool> IsCustomPlaybackOrder(int collectionId) => false.AsTask();
public Task<Option<string>> GetNameFromKey(CollectionKey emptyCollection, CancellationToken cancellationToken) => Option<string>.None.AsTask(); public Task<Option<string>> GetNameFromKey(CollectionKey emptyCollection, CancellationToken cancellationToken) => Option<string>.None.AsTask();

6
ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutChangeDetectionTests.cs

@ -161,7 +161,7 @@ public static class BlockPlayoutChangeDetectionTests
Id = 1, Id = 1,
Index = 1, Index = 1,
BlockId = 1, BlockId = 1,
CollectionType = ProgramScheduleItemCollectionType.TelevisionShow, CollectionType = CollectionType.TelevisionShow,
MediaItemId = 1, MediaItemId = 1,
PlaybackOrder = PlaybackOrder.Chronological PlaybackOrder = PlaybackOrder.Chronological
} }
@ -179,7 +179,7 @@ public static class BlockPlayoutChangeDetectionTests
Id = 2, Id = 2,
Index = 1, Index = 1,
BlockId = 2, BlockId = 2,
CollectionType = ProgramScheduleItemCollectionType.TelevisionShow, CollectionType = CollectionType.TelevisionShow,
MediaItemId = 2, MediaItemId = 2,
PlaybackOrder = PlaybackOrder.Chronological PlaybackOrder = PlaybackOrder.Chronological
} }
@ -197,7 +197,7 @@ public static class BlockPlayoutChangeDetectionTests
Id = 3, Id = 3,
Index = 1, Index = 1,
BlockId = 3, BlockId = 3,
CollectionType = ProgramScheduleItemCollectionType.TelevisionShow, CollectionType = CollectionType.TelevisionShow,
MediaItemId = 3, MediaItemId = 3,
PlaybackOrder = PlaybackOrder.Chronological PlaybackOrder = PlaybackOrder.Chronological
} }

6
ErsatzTV.Core.Tests/Scheduling/ChronologicalContentTests.cs

@ -25,7 +25,7 @@ public class ChronologicalContentTests
{ {
chronologicalContent.Current.IsSome.ShouldBeTrue(); chronologicalContent.Current.IsSome.ShouldBeTrue();
chronologicalContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i); chronologicalContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i);
chronologicalContent.MoveNext(); chronologicalContent.MoveNext(Option<DateTimeOffset>.None);
} }
} }
@ -40,7 +40,7 @@ public class ChronologicalContentTests
for (var i = 0; i < 10; i++) for (var i = 0; i < 10; i++)
{ {
chronologicalContent.State.Index.ShouldBe(i % 10); chronologicalContent.State.Index.ShouldBe(i % 10);
chronologicalContent.MoveNext(); chronologicalContent.MoveNext(Option<DateTimeOffset>.None);
} }
} }
@ -57,7 +57,7 @@ public class ChronologicalContentTests
chronologicalContent.Current.IsSome.ShouldBeTrue(); chronologicalContent.Current.IsSome.ShouldBeTrue();
chronologicalContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i); chronologicalContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i);
chronologicalContent.State.Index.ShouldBe(i - 1); chronologicalContent.State.Index.ShouldBe(i - 1);
chronologicalContent.MoveNext(); chronologicalContent.MoveNext(Option<DateTimeOffset>.None);
} }
} }

6
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ContinuePlayoutTests.cs

@ -541,6 +541,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -548,6 +549,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
@ -661,6 +663,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -668,6 +671,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
@ -783,6 +787,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -790,6 +795,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
PlayoutBuildResult result = await builder.Build( PlayoutBuildResult result = await builder.Build(

22
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs

@ -550,6 +550,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -557,6 +558,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
@ -664,6 +666,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -671,6 +674,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
@ -829,6 +833,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -836,6 +841,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
@ -953,6 +959,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -960,6 +967,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
@ -1076,6 +1084,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -1083,6 +1092,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
@ -1200,6 +1210,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -1207,6 +1218,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
@ -1329,6 +1341,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -1336,6 +1349,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
@ -1458,6 +1472,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -1465,6 +1480,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
@ -1596,6 +1612,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -1603,6 +1620,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
@ -1727,6 +1745,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -1734,6 +1753,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
@ -1824,6 +1844,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -1831,6 +1852,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);

10
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/PlayoutBuilderTestBase.cs

@ -66,6 +66,7 @@ public abstract class PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
collectionRepo, collectionRepo,
@ -73,6 +74,7 @@ public abstract class PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
var items = new List<ProgramScheduleItem> { Flood(mediaCollection, playbackOrder) }; var items = new List<ProgramScheduleItem> { Flood(mediaCollection, playbackOrder) };
@ -120,7 +122,7 @@ public abstract class PlayoutBuilderTestBase
{ {
Id = 1, Id = 1,
Index = 1, Index = 1,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
Collection = mediaCollection, Collection = mediaCollection,
CollectionId = mediaCollection.Id, CollectionId = mediaCollection.Id,
StartTime = null, StartTime = null,
@ -135,7 +137,7 @@ public abstract class PlayoutBuilderTestBase
{ {
Id = 1, Id = 1,
Index = 1, Index = 1,
CollectionType = ProgramScheduleItemCollectionType.SmartCollection, CollectionType = CollectionType.SmartCollection,
SmartCollection = smartCollection, SmartCollection = smartCollection,
SmartCollectionId = smartCollection.Id, SmartCollectionId = smartCollection.Id,
StartTime = null, StartTime = null,
@ -143,7 +145,7 @@ public abstract class PlayoutBuilderTestBase
FallbackFiller = new FillerPreset FallbackFiller = new FillerPreset
{ {
Id = 1, Id = 1,
CollectionType = ProgramScheduleItemCollectionType.SmartCollection, CollectionType = CollectionType.SmartCollection,
SmartCollection = fillerCollection, SmartCollection = fillerCollection,
SmartCollectionId = fillerCollection.Id, SmartCollectionId = fillerCollection.Id,
FillerKind = FillerKind.Fallback FillerKind = FillerKind.Fallback
@ -180,6 +182,7 @@ public abstract class PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
collectionRepo, collectionRepo,
@ -187,6 +190,7 @@ public abstract class PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
var items = new List<ProgramScheduleItem> { Flood(mediaCollection, fillerCollection, playbackOrder) }; var items = new List<ProgramScheduleItem> { Flood(mediaCollection, fillerCollection, playbackOrder) };

4
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/RefreshPlayoutTests.cs

@ -77,7 +77,7 @@ public class RefreshPlayoutTests : PlayoutBuilderTestBase
AnchorDate = HoursAfterMidnight(24).UtcDateTime, AnchorDate = HoursAfterMidnight(24).UtcDateTime,
Collection = collectionOne, Collection = collectionOne,
CollectionId = collectionOne.Id, CollectionId = collectionOne.Id,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
EnumeratorState = new CollectionEnumeratorState EnumeratorState = new CollectionEnumeratorState
{ {
Index = 1, Index = 1,
@ -102,6 +102,7 @@ public class RefreshPlayoutTests : PlayoutBuilderTestBase
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
configRepo, configRepo,
fakeRepository, fakeRepository,
@ -109,6 +110,7 @@ public class RefreshPlayoutTests : PlayoutBuilderTestBase
artistRepo, artistRepo,
factory, factory,
localFileSystem, localFileSystem,
rerunHelper,
Logger); Logger);
DateTimeOffset start = HoursAfterMidnight(24); DateTimeOffset start = HoursAfterMidnight(24);

6
ErsatzTV.Core.Tests/Scheduling/CustomOrderContentTests.cs

@ -25,7 +25,7 @@ public class CustomOrderContentTests
{ {
customOrderContent.Current.IsSome.ShouldBeTrue(); customOrderContent.Current.IsSome.ShouldBeTrue();
customOrderContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i); customOrderContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i);
customOrderContent.MoveNext(); customOrderContent.MoveNext(Option<DateTimeOffset>.None);
} }
} }
@ -41,7 +41,7 @@ public class CustomOrderContentTests
for (var i = 0; i < 10; i++) for (var i = 0; i < 10; i++)
{ {
customOrderContent.State.Index.ShouldBe(i % 10); customOrderContent.State.Index.ShouldBe(i % 10);
customOrderContent.MoveNext(); customOrderContent.MoveNext(Option<DateTimeOffset>.None);
} }
} }
@ -59,7 +59,7 @@ public class CustomOrderContentTests
customOrderContent.Current.IsSome.ShouldBeTrue(); customOrderContent.Current.IsSome.ShouldBeTrue();
customOrderContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i); customOrderContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i);
customOrderContent.State.Index.ShouldBe(5 - i + 5); // 5 through 10 customOrderContent.State.Index.ShouldBe(5 - i + 5); // 5 through 10
customOrderContent.MoveNext(); customOrderContent.MoveNext(Option<DateTimeOffset>.None);
} }
} }

36
ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs

@ -27,7 +27,7 @@ public class PlaylistEnumeratorTests
Id = 1, Id = 1,
PlaybackOrder = PlaybackOrder.Chronological, PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = false, PlayAll = false,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
CollectionId = 1 CollectionId = 1
}, },
[FakeMovie(10)] [FakeMovie(10)]
@ -38,7 +38,7 @@ public class PlaylistEnumeratorTests
Id = 2, Id = 2,
PlaybackOrder = PlaybackOrder.Chronological, PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = true, PlayAll = true,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
CollectionId = 2 CollectionId = 2
}, },
[FakeMovie(20), FakeMovie(21)] [FakeMovie(20), FakeMovie(21)]
@ -49,7 +49,7 @@ public class PlaylistEnumeratorTests
Id = 3, Id = 3,
PlaybackOrder = PlaybackOrder.Chronological, PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = false, PlayAll = false,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
CollectionId = 3 CollectionId = 3
}, },
[FakeMovie(30), FakeMovie(31)] [FakeMovie(30), FakeMovie(31)]
@ -67,11 +67,11 @@ public class PlaylistEnumeratorTests
var items = new List<int>(); var items = new List<int>();
items.AddRange(enumerator.Current.Map(mi => mi.Id)); items.AddRange(enumerator.Current.Map(mi => mi.Id));
enumerator.MoveNext(); enumerator.MoveNext(Option<DateTimeOffset>.None);
while (enumerator.State.Index > 0) while (enumerator.State.Index > 0)
{ {
items.AddRange(enumerator.Current.Map(mi => mi.Id)); items.AddRange(enumerator.Current.Map(mi => mi.Id));
enumerator.MoveNext(); enumerator.MoveNext(Option<DateTimeOffset>.None);
} }
items.Count.ShouldBe(8); items.Count.ShouldBe(8);
@ -95,7 +95,7 @@ public class PlaylistEnumeratorTests
Id = 1, Id = 1,
PlaybackOrder = PlaybackOrder.Chronological, PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = false, PlayAll = false,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
CollectionId = 1 CollectionId = 1
}, },
[FakeMovie(10)] [FakeMovie(10)]
@ -106,7 +106,7 @@ public class PlaylistEnumeratorTests
Id = 2, Id = 2,
PlaybackOrder = PlaybackOrder.Chronological, PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = false, PlayAll = false,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
CollectionId = 2 CollectionId = 2
}, },
[FakeMovie(20), FakeMovie(21)] [FakeMovie(20), FakeMovie(21)]
@ -117,7 +117,7 @@ public class PlaylistEnumeratorTests
Id = 3, Id = 3,
PlaybackOrder = PlaybackOrder.Chronological, PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = true, PlayAll = true,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
CollectionId = 3 CollectionId = 3
}, },
[FakeMovie(30), FakeMovie(31)] [FakeMovie(30), FakeMovie(31)]
@ -135,11 +135,11 @@ public class PlaylistEnumeratorTests
var items = new List<int>(); var items = new List<int>();
items.AddRange(enumerator.Current.Map(mi => mi.Id)); items.AddRange(enumerator.Current.Map(mi => mi.Id));
enumerator.MoveNext(); enumerator.MoveNext(Option<DateTimeOffset>.None);
while (enumerator.State.Index > 0) while (enumerator.State.Index > 0)
{ {
items.AddRange(enumerator.Current.Map(mi => mi.Id)); items.AddRange(enumerator.Current.Map(mi => mi.Id));
enumerator.MoveNext(); enumerator.MoveNext(Option<DateTimeOffset>.None);
} }
items.Count.ShouldBe(8); items.Count.ShouldBe(8);
@ -161,7 +161,7 @@ public class PlaylistEnumeratorTests
Index = 0, Index = 0,
PlaybackOrder = PlaybackOrder.Chronological, PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = false, PlayAll = false,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
CollectionId = 1 CollectionId = 1
}, },
[FakeMovie(10)] [FakeMovie(10)]
@ -173,7 +173,7 @@ public class PlaylistEnumeratorTests
Index = 1, Index = 1,
PlaybackOrder = PlaybackOrder.Chronological, PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = true, PlayAll = true,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
CollectionId = 2 CollectionId = 2
}, },
[FakeMovie(20), FakeMovie(21)] [FakeMovie(20), FakeMovie(21)]
@ -185,7 +185,7 @@ public class PlaylistEnumeratorTests
Index = 2, Index = 2,
PlaybackOrder = PlaybackOrder.Chronological, PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = false, PlayAll = false,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
CollectionId = 3 CollectionId = 3
}, },
[FakeMovie(30)] [FakeMovie(30)]
@ -206,7 +206,7 @@ public class PlaylistEnumeratorTests
for (var i = 0; i < 4; i++) for (var i = 0; i < 4; i++)
{ {
items.AddRange(enumerator.Current.Map(mi => mi.Id)); items.AddRange(enumerator.Current.Map(mi => mi.Id));
enumerator.MoveNext(); enumerator.MoveNext(Option<DateTimeOffset>.None);
} }
// with seed 1, shuffle order of (1,2,3) is (2,3,1) // with seed 1, shuffle order of (1,2,3) is (2,3,1)
@ -231,7 +231,7 @@ public class PlaylistEnumeratorTests
PlaybackOrder = PlaybackOrder.Chronological, PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = false, PlayAll = false,
Count = 2, Count = 2,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
CollectionId = 1 CollectionId = 1
}, },
[FakeMovie(10), FakeMovie(11), FakeMovie(12)] [FakeMovie(10), FakeMovie(11), FakeMovie(12)]
@ -243,7 +243,7 @@ public class PlaylistEnumeratorTests
Index = 1, Index = 1,
PlaybackOrder = PlaybackOrder.Chronological, PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = false, PlayAll = false,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
CollectionId = 2 CollectionId = 2
}, },
[FakeMovie(20)] [FakeMovie(20)]
@ -255,7 +255,7 @@ public class PlaylistEnumeratorTests
Index = 2, Index = 2,
PlaybackOrder = PlaybackOrder.Chronological, PlaybackOrder = PlaybackOrder.Chronological,
PlayAll = false, PlayAll = false,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
CollectionId = 3 CollectionId = 3
}, },
[FakeMovie(30)] [FakeMovie(30)]
@ -276,7 +276,7 @@ public class PlaylistEnumeratorTests
for (var i = 0; i < 4; i++) for (var i = 0; i < 4; i++)
{ {
items.AddRange(enumerator.Current.Map(mi => mi.Id)); items.AddRange(enumerator.Current.Map(mi => mi.Id));
enumerator.MoveNext(); enumerator.MoveNext(Option<DateTimeOffset>.None);
} }
// with seed 1, shuffle order of (1,2,3) is (2,3,1) // with seed 1, shuffle order of (1,2,3) is (2,3,1)

6
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs

@ -25,7 +25,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
{ {
Id = 1, Id = 1,
Index = 1, Index = 1,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
Collection = collectionOne, Collection = collectionOne,
CollectionId = collectionOne.Id, CollectionId = collectionOne.Id,
StartTime = TimeSpan.FromHours(1), StartTime = TimeSpan.FromHours(1),
@ -127,7 +127,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
{ {
Id = 1, Id = 1,
Index = 1, Index = 1,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
Collection = collectionOne, Collection = collectionOne,
CollectionId = collectionOne.Id, CollectionId = collectionOne.Id,
StartTime = TimeSpan.FromHours(1), StartTime = TimeSpan.FromHours(1),
@ -199,7 +199,7 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
{ {
Id = 1, Id = 1,
Index = 1, Index = 1,
CollectionType = ProgramScheduleItemCollectionType.Collection, CollectionType = CollectionType.Collection,
Collection = collectionOne, Collection = collectionOne,
CollectionId = collectionOne.Id, CollectionId = collectionOne.Id,
StartTime = null, StartTime = null,

6
ErsatzTV.Core.Tests/Scheduling/RandomizedContentTests.cs

@ -37,7 +37,7 @@ public class RandomizedContentTests
randomizedContent.Current.IsSome.ShouldBeTrue(); randomizedContent.Current.IsSome.ShouldBeTrue();
randomizedContent.Current.Do(c => list.Add(c.Id)); randomizedContent.Current.Do(c => list.Add(c.Id));
randomizedContent.MoveNext(); randomizedContent.MoveNext(Option<DateTimeOffset>.None);
} }
list.ShouldNotBe([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); list.ShouldNotBe([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
@ -56,7 +56,7 @@ public class RandomizedContentTests
{ {
randomizedContent.State.Index.ShouldBe(i); randomizedContent.State.Index.ShouldBe(i);
randomizedContent.MoveNext(); randomizedContent.MoveNext(Option<DateTimeOffset>.None);
} }
} }
@ -75,7 +75,7 @@ public class RandomizedContentTests
randomizedContent.Current.Map(c => c.Id).IfNone(-1).ShouldBe(_expected[i - 2]); randomizedContent.Current.Map(c => c.Id).IfNone(-1).ShouldBe(_expected[i - 2]);
randomizedContent.State.Index.ShouldBe(i); randomizedContent.State.Index.ShouldBe(i);
randomizedContent.MoveNext(); randomizedContent.MoveNext(Option<DateTimeOffset>.None);
} }
} }

2
ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs

@ -125,6 +125,7 @@ public class ScheduleIntegrationTests
new ArtistRepository(factory), new ArtistRepository(factory),
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(), Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(),
Substitute.For<ILocalFileSystem>(), Substitute.For<ILocalFileSystem>(),
Substitute.For<IRerunHelper>(),
provider.GetRequiredService<ILogger<PlayoutBuilder>>()); provider.GetRequiredService<ILogger<PlayoutBuilder>>());
{ {
@ -320,6 +321,7 @@ public class ScheduleIntegrationTests
new ArtistRepository(factory), new ArtistRepository(factory),
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(), Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(),
Substitute.For<ILocalFileSystem>(), Substitute.For<ILocalFileSystem>(),
Substitute.For<IRerunHelper>(),
provider.GetRequiredService<ILogger<PlayoutBuilder>>()); provider.GetRequiredService<ILogger<PlayoutBuilder>>());
for (var i = 0; i <= 24 * 4; i++) for (var i = 0; i <= 24 * 4; i++)

8
ErsatzTV.Core.Tests/Scheduling/SeasonEpisodeContentTests.cs

@ -25,7 +25,7 @@ public class SeasonEpisodeContentTests
{ {
chronologicalContent.Current.IsSome.ShouldBeTrue(); chronologicalContent.Current.IsSome.ShouldBeTrue();
chronologicalContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i); chronologicalContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i);
chronologicalContent.MoveNext(); chronologicalContent.MoveNext(Option<DateTimeOffset>.None);
} }
} }
@ -40,7 +40,7 @@ public class SeasonEpisodeContentTests
for (var i = 0; i < 10; i++) for (var i = 0; i < 10; i++)
{ {
chronologicalContent.State.Index.ShouldBe(i % 10); chronologicalContent.State.Index.ShouldBe(i % 10);
chronologicalContent.MoveNext(); chronologicalContent.MoveNext(Option<DateTimeOffset>.None);
} }
} }
@ -57,7 +57,7 @@ public class SeasonEpisodeContentTests
chronologicalContent.Current.IsSome.ShouldBeTrue(); chronologicalContent.Current.IsSome.ShouldBeTrue();
chronologicalContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i); chronologicalContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i);
chronologicalContent.State.Index.ShouldBe(i - 1); chronologicalContent.State.Index.ShouldBe(i - 1);
chronologicalContent.MoveNext(); chronologicalContent.MoveNext(Option<DateTimeOffset>.None);
} }
} }
@ -89,7 +89,7 @@ public class SeasonEpisodeContentTests
for (var i = 0; i < 16; i++) for (var i = 0; i < 16; i++)
{ {
chronologicalContent.State.Index.ShouldBe(i % 8); chronologicalContent.State.Index.ShouldBe(i % 8);
chronologicalContent.MoveNext(); chronologicalContent.MoveNext(Option<DateTimeOffset>.None);
} }
} }

10
ErsatzTV.Core.Tests/Scheduling/ShuffledContentTests.cs

@ -32,7 +32,7 @@ public class ShuffledContentTests
{ {
shuffledContent.Current.IsSome.ShouldBeTrue(); shuffledContent.Current.IsSome.ShouldBeTrue();
shuffledContent.Current.Do(x => list.Add(x.Id)); shuffledContent.Current.Do(x => list.Add(x.Id));
shuffledContent.MoveNext(); shuffledContent.MoveNext(Option<DateTimeOffset>.None);
} }
for (var i = 0; i < list.Count - 1; i++) for (var i = 0; i < list.Count - 1; i++)
@ -59,7 +59,7 @@ public class ShuffledContentTests
{ {
shuffledContent.Current.IsSome.ShouldBeTrue(); shuffledContent.Current.IsSome.ShouldBeTrue();
shuffledContent.Current.Do(x => list.Add(x.Id)); shuffledContent.Current.Do(x => list.Add(x.Id));
shuffledContent.MoveNext(); shuffledContent.MoveNext(Option<DateTimeOffset>.None);
} }
list.ShouldBe([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); list.ShouldBe([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
@ -80,7 +80,7 @@ public class ShuffledContentTests
{ {
shuffledContent.Current.IsSome.ShouldBeTrue(); shuffledContent.Current.IsSome.ShouldBeTrue();
shuffledContent.Current.Do(x => list.Add(x.Id)); shuffledContent.Current.Do(x => list.Add(x.Id));
shuffledContent.MoveNext(); shuffledContent.MoveNext(Option<DateTimeOffset>.None);
} }
list.ShouldNotBe([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); list.ShouldNotBe([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
@ -99,7 +99,7 @@ public class ShuffledContentTests
for (var i = 0; i < 10; i++) for (var i = 0; i < 10; i++)
{ {
shuffledContent.State.Index.ShouldBe(i); shuffledContent.State.Index.ShouldBe(i);
shuffledContent.MoveNext(); shuffledContent.MoveNext(Option<DateTimeOffset>.None);
} }
} }
@ -117,7 +117,7 @@ public class ShuffledContentTests
shuffledContent.Current.IsSome.ShouldBeTrue(); shuffledContent.Current.IsSome.ShouldBeTrue();
shuffledContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i); shuffledContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i);
shuffledContent.State.Index.ShouldBe(i - 1); shuffledContent.State.Index.ShouldBe(i - 1);
shuffledContent.MoveNext(); shuffledContent.MoveNext(Option<DateTimeOffset>.None);
} }
} }

2
ErsatzTV.Core/Domain/Collection/PlaylistItem.cs

@ -6,7 +6,7 @@ public class PlaylistItem
public int Index { get; set; } public int Index { get; set; }
public int PlaylistId { get; set; } public int PlaylistId { get; set; }
public Playlist Playlist { get; set; } public Playlist Playlist { get; set; }
public ProgramScheduleItemCollectionType CollectionType { get; set; } public CollectionType CollectionType { get; set; }
public int? CollectionId { get; set; } public int? CollectionId { get; set; }
public Collection Collection { get; set; } public Collection Collection { get; set; }
public int? MediaItemId { get; set; } public int? MediaItemId { get; set; }

21
ErsatzTV.Core/Domain/Collection/RerunCollection.cs

@ -0,0 +1,21 @@
using System.Diagnostics.CodeAnalysis;
namespace ErsatzTV.Core.Domain;
[SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix")]
public class RerunCollection
{
public int Id { get; set; }
public string Name { get; set; }
public CollectionType CollectionType { get; set; }
public int? CollectionId { get; set; }
public Collection Collection { get; set; }
public int? MediaItemId { get; set; }
public MediaItem MediaItem { get; set; }
public int? MultiCollectionId { get; set; }
public MultiCollection MultiCollection { get; set; }
public int? SmartCollectionId { get; set; }
public SmartCollection SmartCollection { get; set; }
public PlaybackOrder FirstRunPlaybackOrder { get; set; }
public PlaybackOrder RerunPlaybackOrder { get; set; }
}

4
ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs → ErsatzTV.Core/Domain/CollectionType.cs

@ -1,6 +1,6 @@
namespace ErsatzTV.Core.Domain; namespace ErsatzTV.Core.Domain;
public enum ProgramScheduleItemCollectionType public enum CollectionType
{ {
Collection = 0, Collection = 0,
TelevisionShow = 1, TelevisionShow = 1,
@ -9,6 +9,8 @@ public enum ProgramScheduleItemCollectionType
MultiCollection = 4, MultiCollection = 4,
SmartCollection = 5, SmartCollection = 5,
Playlist = 6, Playlist = 6,
RerunFirstRun = 7,
RerunRerun = 8,
Movie = 10, Movie = 10,
Episode = 20, Episode = 20,

1
ErsatzTV.Core/Domain/ConfigElementKey.cs

@ -34,6 +34,7 @@ public class ConfigElementKey
public static ConfigElementKey CollectionsPageSize => new("pages.collections.page_size"); public static ConfigElementKey CollectionsPageSize => new("pages.collections.page_size");
public static ConfigElementKey MultiCollectionsPageSize => new("pages.multi_collections.page_size"); public static ConfigElementKey MultiCollectionsPageSize => new("pages.multi_collections.page_size");
public static ConfigElementKey SmartCollectionsPageSize => new("pages.smart_collections.page_size"); public static ConfigElementKey SmartCollectionsPageSize => new("pages.smart_collections.page_size");
public static ConfigElementKey RerunCollectionsPageSize => new("pages.rerun_collections.page_size");
public static ConfigElementKey SchedulesPageSize => new("pages.schedules.page_size"); public static ConfigElementKey SchedulesPageSize => new("pages.schedules.page_size");
public static ConfigElementKey SchedulesDetailPageSize => new("pages.schedules.detail_page_size"); public static ConfigElementKey SchedulesDetailPageSize => new("pages.schedules.detail_page_size");
public static ConfigElementKey PlayoutsPageSize => new("pages.playouts.page_size"); public static ConfigElementKey PlayoutsPageSize => new("pages.playouts.page_size");

2
ErsatzTV.Core/Domain/Filler/FillerPreset.cs

@ -10,7 +10,7 @@ public class FillerPreset
public int? Count { get; set; } public int? Count { get; set; }
public int? PadToNearestMinute { get; set; } public int? PadToNearestMinute { get; set; }
public bool AllowWatermarks { get; set; } public bool AllowWatermarks { get; set; }
public ProgramScheduleItemCollectionType CollectionType { get; set; } public CollectionType CollectionType { get; set; }
public int? CollectionId { get; set; } public int? CollectionId { get; set; }
public Collection Collection { get; set; } public Collection Collection { get; set; }
public int? MediaItemId { get; set; } public int? MediaItemId { get; set; }

4
ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs

@ -16,13 +16,15 @@ public class PlayoutProgramScheduleAnchor
? new DateTimeOffset(AnchorDate.Value, TimeSpan.Zero).ToLocalTime() ? new DateTimeOffset(AnchorDate.Value, TimeSpan.Zero).ToLocalTime()
: null; : null;
public ProgramScheduleItemCollectionType CollectionType { get; set; } public CollectionType CollectionType { get; set; }
public int? CollectionId { get; set; } public int? CollectionId { get; set; }
public Collection Collection { get; set; } public Collection Collection { get; set; }
public int? MultiCollectionId { get; set; } public int? MultiCollectionId { get; set; }
public MultiCollection MultiCollection { get; set; } public MultiCollection MultiCollection { get; set; }
public int? SmartCollectionId { get; set; } public int? SmartCollectionId { get; set; }
public SmartCollection SmartCollection { get; set; } public SmartCollection SmartCollection { get; set; }
public int? RerunCollectionId { get; set; }
public RerunCollection RerunCollection { get; set; }
public int? MediaItemId { get; set; } public int? MediaItemId { get; set; }
public MediaItem MediaItem { get; set; } public MediaItem MediaItem { get; set; }
public int? PlaylistId { get; set; } public int? PlaylistId { get; set; }

4
ErsatzTV.Core/Domain/ProgramScheduleItem.cs

@ -11,7 +11,7 @@ public abstract class ProgramScheduleItem
public StartType StartType => StartTime.HasValue ? StartType.Fixed : StartType.Dynamic; public StartType StartType => StartTime.HasValue ? StartType.Fixed : StartType.Dynamic;
public TimeSpan? StartTime { get; set; } public TimeSpan? StartTime { get; set; }
public FixedStartTimeBehavior? FixedStartTimeBehavior { get; set; } public FixedStartTimeBehavior? FixedStartTimeBehavior { get; set; }
public ProgramScheduleItemCollectionType CollectionType { get; set; } public CollectionType CollectionType { get; set; }
public GuideMode GuideMode { get; set; } public GuideMode GuideMode { get; set; }
public string CustomTitle { get; set; } public string CustomTitle { get; set; }
public int ProgramScheduleId { get; set; } public int ProgramScheduleId { get; set; }
@ -27,6 +27,8 @@ public abstract class ProgramScheduleItem
public MultiCollection MultiCollection { get; set; } public MultiCollection MultiCollection { get; set; }
public int? SmartCollectionId { get; set; } public int? SmartCollectionId { get; set; }
public SmartCollection SmartCollection { get; set; } public SmartCollection SmartCollection { get; set; }
public int? RerunCollectionId { get; set; }
public RerunCollection RerunCollection { get; set; }
public int? PlaylistId { get; set; } public int? PlaylistId { get; set; }
public Playlist Playlist { get; set; } public Playlist Playlist { get; set; }
public string FakeCollectionKey { get; set; } public string FakeCollectionKey { get; set; }

2
ErsatzTV.Core/Domain/Scheduling/BlockItem.cs

@ -6,7 +6,7 @@ public class BlockItem
public int Index { get; set; } public int Index { get; set; }
public int BlockId { get; set; } public int BlockId { get; set; }
public Block Block { get; set; } public Block Block { get; set; }
public ProgramScheduleItemCollectionType CollectionType { get; set; } public CollectionType CollectionType { get; set; }
public int? CollectionId { get; set; } public int? CollectionId { get; set; }
public Collection Collection { get; set; } public Collection Collection { get; set; }
public int? MediaItemId { get; set; } public int? MediaItemId { get; set; }

4
ErsatzTV.Core/Domain/Scheduling/Deco.cs

@ -22,7 +22,7 @@ public class Deco
// default filler // default filler
public DecoMode DefaultFillerMode { get; set; } public DecoMode DefaultFillerMode { get; set; }
public ProgramScheduleItemCollectionType DefaultFillerCollectionType { get; set; } public CollectionType DefaultFillerCollectionType { get; set; }
public int? DefaultFillerCollectionId { get; set; } public int? DefaultFillerCollectionId { get; set; }
public Collection DefaultFillerCollection { get; set; } public Collection DefaultFillerCollection { get; set; }
public int? DefaultFillerMediaItemId { get; set; } public int? DefaultFillerMediaItemId { get; set; }
@ -35,7 +35,7 @@ public class Deco
// dead air fallback // dead air fallback
public DecoMode DeadAirFallbackMode { get; set; } public DecoMode DeadAirFallbackMode { get; set; }
public ProgramScheduleItemCollectionType DeadAirFallbackCollectionType { get; set; } public CollectionType DeadAirFallbackCollectionType { get; set; }
public int? DeadAirFallbackCollectionId { get; set; } public int? DeadAirFallbackCollectionId { get; set; }
public Collection DeadAirFallbackCollection { get; set; } public Collection DeadAirFallbackCollection { get; set; }
public int? DeadAirFallbackMediaItemId { get; set; } public int? DeadAirFallbackMediaItemId { get; set; }

13
ErsatzTV.Core/Domain/Scheduling/RerunHistory.cs

@ -0,0 +1,13 @@
namespace ErsatzTV.Core.Domain.Scheduling;
public class RerunHistory
{
public int Id { get; set; }
public int PlayoutId { get; set; }
public Playout Playout { get; set; }
public int RerunCollectionId { get; set; }
public RerunCollection RerunCollection { get; set; }
public int MediaItemId { get; set; }
public MediaItem MediaItem { get; set; }
public DateTime When { get; set; }
}

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

@ -29,6 +29,7 @@ public interface IMediaCollectionRepository
string query, string query,
string smartCollectionName, string smartCollectionName,
CancellationToken cancellationToken); CancellationToken cancellationToken);
Task<List<MediaItem>> GetRerunCollectionItems(int id, CancellationToken cancellationToken);
Task<List<MediaItem>> GetShowItemsByShowGuids(List<string> guids); Task<List<MediaItem>> GetShowItemsByShowGuids(List<string> guids);
Task<List<MediaItem>> GetPlaylistItems(int id, CancellationToken cancellationToken); Task<List<MediaItem>> GetPlaylistItems(int id, CancellationToken cancellationToken);
Task<List<Movie>> GetMovie(int id); Task<List<Movie>> GetMovie(int id);
@ -48,6 +49,7 @@ public interface IMediaCollectionRepository
Task<List<int>> PlayoutIdsUsingCollection(int collectionId); Task<List<int>> PlayoutIdsUsingCollection(int collectionId);
Task<List<int>> PlayoutIdsUsingMultiCollection(int multiCollectionId); Task<List<int>> PlayoutIdsUsingMultiCollection(int multiCollectionId);
Task<List<int>> PlayoutIdsUsingSmartCollection(int smartCollectionId); Task<List<int>> PlayoutIdsUsingSmartCollection(int smartCollectionId);
Task<List<int>> PlayoutIdsUsingRerunCollection(int rerunCollectionId);
Task<bool> IsCustomPlaybackOrder(int collectionId); Task<bool> IsCustomPlaybackOrder(int collectionId);
Task<Option<string>> GetNameFromKey(CollectionKey emptyCollection, CancellationToken cancellationToken); Task<Option<string>> GetNameFromKey(CollectionKey emptyCollection, CancellationToken cancellationToken);
List<CollectionWithItems> GroupIntoFakeCollections(List<MediaItem> items, string fakeKey = null); List<CollectionWithItems> GroupIntoFakeCollections(List<MediaItem> items, string fakeKey = null);

2
ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs

@ -10,5 +10,5 @@ public interface IMediaCollectionEnumerator
int Count { get; } int Count { get; }
Option<TimeSpan> MinimumDuration { get; } Option<TimeSpan> MinimumDuration { get; }
void ResetState(CollectionEnumeratorState state); void ResetState(CollectionEnumeratorState state);
void MoveNext(); void MoveNext(Option<DateTimeOffset> scheduledAt);
} }

35
ErsatzTV.Core/Interfaces/Scheduling/IRerunHelper.cs

@ -0,0 +1,35 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IRerunHelper
{
bool ClearHistory { get; set; }
Task InitWithMediaItems(
int playoutId,
CollectionKey collectionKey,
List<MediaItem> mediaItems,
CancellationToken cancellationToken);
IMediaCollectionEnumerator CreateEnumerator(
CollectionKey collectionKey,
CollectionEnumeratorState state,
CancellationToken cancellationToken);
bool IsFirstRun(CollectionKey collectionKey, int mediaItemId);
bool IsRerun(CollectionKey collectionKey, int mediaItemId);
int FirstRunCount(CollectionKey collectionKey);
int RerunCount(CollectionKey collectionKey);
void AddToHistory(CollectionKey collectionKey, int mediaItemId, DateTimeOffset scheduledAt);
System.Collections.Generic.HashSet<int> GetHistoryToRemove();
List<RerunHistory> GetHistoryToAdd();
}

2
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs

@ -242,7 +242,7 @@ public class BlockPlayoutBuilder(
result.AddedHistory.Add(nextHistory); result.AddedHistory.Add(nextHistory);
currentTime += itemDuration; currentTime += itemDuration;
enumerator.MoveNext(); enumerator.MoveNext(playoutItem.StartOffset);
} }
if (pastTime) if (pastTime)

14
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs

@ -177,7 +177,7 @@ public class BlockPlayoutFillerBuilder(
result.AddedHistory.Add(nextHistory); result.AddedHistory.Add(nextHistory);
current += itemDuration; current += itemDuration;
enumerator.MoveNext(); enumerator.MoveNext(filler.StartOffset);
} }
if (pastTime) if (pastTime)
@ -227,17 +227,17 @@ public class BlockPlayoutFillerBuilder(
{ {
switch (deco.DefaultFillerCollectionType) switch (deco.DefaultFillerCollectionType)
{ {
case ProgramScheduleItemCollectionType.Collection: case CollectionType.Collection:
return deco.DefaultFillerCollectionId.HasValue; return deco.DefaultFillerCollectionId.HasValue;
case ProgramScheduleItemCollectionType.TelevisionShow: case CollectionType.TelevisionShow:
return deco.DefaultFillerMediaItemId.HasValue; return deco.DefaultFillerMediaItemId.HasValue;
case ProgramScheduleItemCollectionType.TelevisionSeason: case CollectionType.TelevisionSeason:
return deco.DefaultFillerMediaItemId.HasValue; return deco.DefaultFillerMediaItemId.HasValue;
case ProgramScheduleItemCollectionType.Artist: case CollectionType.Artist:
return deco.DefaultFillerMediaItemId.HasValue; return deco.DefaultFillerMediaItemId.HasValue;
case ProgramScheduleItemCollectionType.MultiCollection: case CollectionType.MultiCollection:
return deco.DefaultFillerMultiCollectionId.HasValue; return deco.DefaultFillerMultiCollectionId.HasValue;
case ProgramScheduleItemCollectionType.SmartCollection: case CollectionType.SmartCollection:
return deco.DefaultFillerSmartCollectionId.HasValue; return deco.DefaultFillerSmartCollectionId.HasValue;
default: default:
return false; return false;

2
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutShuffledMediaCollectionEnumerator.cs

@ -45,7 +45,7 @@ public class BlockPlayoutShuffledMediaCollectionEnumerator : IMediaCollectionEnu
public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None; public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
public Option<bool> CurrentIncludeInProgramGuide { get; } public Option<bool> CurrentIncludeInProgramGuide { get; }
public void MoveNext() public void MoveNext(Option<DateTimeOffset> scheduledAt)
{ {
State.Index++; State.Index++;
if (State.Index % _mediaItemCount == 0) if (State.Index % _mediaItemCount == 0)

5
ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs

@ -32,7 +32,7 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu
while (State.Index < state.Index) while (State.Index < state.Index)
{ {
MoveNext(); MoveNext(Option<DateTimeOffset>.None);
} }
} }
@ -45,7 +45,8 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu
public Option<MediaItem> Current => _sortedMediaItems.Count != 0 ? _sortedMediaItems[State.Index] : None; public Option<MediaItem> Current => _sortedMediaItems.Count != 0 ? _sortedMediaItems[State.Index] : None;
public Option<bool> CurrentIncludeInProgramGuide { get; } public Option<bool> CurrentIncludeInProgramGuide { get; }
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count; public void MoveNext(Option<DateTimeOffset> scheduledAt) =>
State.Index = (State.Index + 1) % _sortedMediaItems.Count;
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value; public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;

125
ErsatzTV.Core/Scheduling/CollectionKey.cs

@ -6,10 +6,11 @@ namespace ErsatzTV.Core.Scheduling;
public class CollectionKey : Record<CollectionKey> public class CollectionKey : Record<CollectionKey>
{ {
public ProgramScheduleItemCollectionType CollectionType { get; set; } public CollectionType CollectionType { get; set; }
public int? CollectionId { get; set; } public int? CollectionId { get; set; }
public int? MultiCollectionId { get; set; } public int? MultiCollectionId { get; set; }
public int? SmartCollectionId { get; set; } public int? SmartCollectionId { get; set; }
public int? RerunCollectionId { get; set; }
public int? MediaItemId { get; set; } public int? MediaItemId { get; set; }
public int? PlaylistId { get; set; } public int? PlaylistId { get; set; }
public string FakeCollectionKey { get; set; } public string FakeCollectionKey { get; set; }
@ -17,71 +18,71 @@ public class CollectionKey : Record<CollectionKey>
public static CollectionKey ForPlaylistItem(PlaylistItem item) => public static CollectionKey ForPlaylistItem(PlaylistItem item) =>
item.CollectionType switch item.CollectionType switch
{ {
ProgramScheduleItemCollectionType.Collection => new CollectionKey CollectionType.Collection => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
CollectionId = item.CollectionId CollectionId = item.CollectionId
}, },
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey CollectionType.TelevisionShow => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey CollectionType.TelevisionSeason => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.Artist => new CollectionKey CollectionType.Artist => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey CollectionType.MultiCollection => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MultiCollectionId = item.MultiCollectionId MultiCollectionId = item.MultiCollectionId
}, },
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey CollectionType.SmartCollection => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
SmartCollectionId = item.SmartCollectionId SmartCollectionId = item.SmartCollectionId
}, },
ProgramScheduleItemCollectionType.FakeCollection => new CollectionKey CollectionType.FakeCollection => new CollectionKey
{ {
CollectionType = item.CollectionType CollectionType = item.CollectionType
}, },
ProgramScheduleItemCollectionType.FakePlaylistItem => new CollectionKey CollectionType.FakePlaylistItem => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
CollectionId = item.CollectionId CollectionId = item.CollectionId
}, },
ProgramScheduleItemCollectionType.Movie => new CollectionKey CollectionType.Movie => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.Episode => new CollectionKey CollectionType.Episode => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.MusicVideo => new CollectionKey CollectionType.MusicVideo => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.OtherVideo => new CollectionKey CollectionType.OtherVideo => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.Song => new CollectionKey CollectionType.Song => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.Image => new CollectionKey CollectionType.Image => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
@ -92,66 +93,66 @@ public class CollectionKey : Record<CollectionKey>
public static CollectionKey ForBlockItem(BlockItem item) => public static CollectionKey ForBlockItem(BlockItem item) =>
item.CollectionType switch item.CollectionType switch
{ {
ProgramScheduleItemCollectionType.Collection => new CollectionKey CollectionType.Collection => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
CollectionId = item.CollectionId CollectionId = item.CollectionId
}, },
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey CollectionType.TelevisionShow => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey CollectionType.TelevisionSeason => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.Artist => new CollectionKey CollectionType.Artist => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey CollectionType.MultiCollection => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MultiCollectionId = item.MultiCollectionId MultiCollectionId = item.MultiCollectionId
}, },
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey CollectionType.SmartCollection => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
SmartCollectionId = item.SmartCollectionId SmartCollectionId = item.SmartCollectionId
}, },
ProgramScheduleItemCollectionType.FakeCollection => new CollectionKey CollectionType.FakeCollection => new CollectionKey
{ {
CollectionType = item.CollectionType CollectionType = item.CollectionType
}, },
ProgramScheduleItemCollectionType.Movie => new CollectionKey CollectionType.Movie => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.Episode => new CollectionKey CollectionType.Episode => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.MusicVideo => new CollectionKey CollectionType.MusicVideo => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.OtherVideo => new CollectionKey CollectionType.OtherVideo => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.Song => new CollectionKey CollectionType.Song => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
}, },
ProgramScheduleItemCollectionType.Image => new CollectionKey CollectionType.Image => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId MediaItemId = item.MediaItemId
@ -162,66 +163,66 @@ public class CollectionKey : Record<CollectionKey>
public static CollectionKey ForDecoDefaultFiller(Deco deco) => public static CollectionKey ForDecoDefaultFiller(Deco deco) =>
deco.DefaultFillerCollectionType switch deco.DefaultFillerCollectionType switch
{ {
ProgramScheduleItemCollectionType.Collection => new CollectionKey CollectionType.Collection => new CollectionKey
{ {
CollectionType = deco.DefaultFillerCollectionType, CollectionType = deco.DefaultFillerCollectionType,
CollectionId = deco.DefaultFillerCollectionId CollectionId = deco.DefaultFillerCollectionId
}, },
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey CollectionType.TelevisionShow => new CollectionKey
{ {
CollectionType = deco.DefaultFillerCollectionType, CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId MediaItemId = deco.DefaultFillerMediaItemId
}, },
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey CollectionType.TelevisionSeason => new CollectionKey
{ {
CollectionType = deco.DefaultFillerCollectionType, CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId MediaItemId = deco.DefaultFillerMediaItemId
}, },
ProgramScheduleItemCollectionType.Artist => new CollectionKey CollectionType.Artist => new CollectionKey
{ {
CollectionType = deco.DefaultFillerCollectionType, CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId MediaItemId = deco.DefaultFillerMediaItemId
}, },
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey CollectionType.MultiCollection => new CollectionKey
{ {
CollectionType = deco.DefaultFillerCollectionType, CollectionType = deco.DefaultFillerCollectionType,
MultiCollectionId = deco.DefaultFillerMultiCollectionId MultiCollectionId = deco.DefaultFillerMultiCollectionId
}, },
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey CollectionType.SmartCollection => new CollectionKey
{ {
CollectionType = deco.DefaultFillerCollectionType, CollectionType = deco.DefaultFillerCollectionType,
SmartCollectionId = deco.DefaultFillerSmartCollectionId SmartCollectionId = deco.DefaultFillerSmartCollectionId
}, },
ProgramScheduleItemCollectionType.FakeCollection => new CollectionKey CollectionType.FakeCollection => new CollectionKey
{ {
CollectionType = deco.DefaultFillerCollectionType CollectionType = deco.DefaultFillerCollectionType
}, },
ProgramScheduleItemCollectionType.Movie => new CollectionKey CollectionType.Movie => new CollectionKey
{ {
CollectionType = deco.DefaultFillerCollectionType, CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId MediaItemId = deco.DefaultFillerMediaItemId
}, },
ProgramScheduleItemCollectionType.Episode => new CollectionKey CollectionType.Episode => new CollectionKey
{ {
CollectionType = deco.DefaultFillerCollectionType, CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId MediaItemId = deco.DefaultFillerMediaItemId
}, },
ProgramScheduleItemCollectionType.MusicVideo => new CollectionKey CollectionType.MusicVideo => new CollectionKey
{ {
CollectionType = deco.DefaultFillerCollectionType, CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId MediaItemId = deco.DefaultFillerMediaItemId
}, },
ProgramScheduleItemCollectionType.OtherVideo => new CollectionKey CollectionType.OtherVideo => new CollectionKey
{ {
CollectionType = deco.DefaultFillerCollectionType, CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId MediaItemId = deco.DefaultFillerMediaItemId
}, },
ProgramScheduleItemCollectionType.Song => new CollectionKey CollectionType.Song => new CollectionKey
{ {
CollectionType = deco.DefaultFillerCollectionType, CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId MediaItemId = deco.DefaultFillerMediaItemId
}, },
ProgramScheduleItemCollectionType.Image => new CollectionKey CollectionType.Image => new CollectionKey
{ {
CollectionType = deco.DefaultFillerCollectionType, CollectionType = deco.DefaultFillerCollectionType,
MediaItemId = deco.DefaultFillerMediaItemId MediaItemId = deco.DefaultFillerMediaItemId
@ -232,49 +233,61 @@ public class CollectionKey : Record<CollectionKey>
public static CollectionKey ForScheduleItem(ProgramScheduleItem item) => public static CollectionKey ForScheduleItem(ProgramScheduleItem item) =>
item.CollectionType switch item.CollectionType switch
{ {
ProgramScheduleItemCollectionType.Collection => new CollectionKey CollectionType.Collection => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
CollectionId = item.CollectionId, CollectionId = item.CollectionId,
FakeCollectionKey = item.FakeCollectionKey FakeCollectionKey = item.FakeCollectionKey
}, },
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey CollectionType.TelevisionShow => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
FakeCollectionKey = item.FakeCollectionKey FakeCollectionKey = item.FakeCollectionKey
}, },
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey CollectionType.TelevisionSeason => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
FakeCollectionKey = item.FakeCollectionKey FakeCollectionKey = item.FakeCollectionKey
}, },
ProgramScheduleItemCollectionType.Artist => new CollectionKey CollectionType.Artist => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
FakeCollectionKey = item.FakeCollectionKey FakeCollectionKey = item.FakeCollectionKey
}, },
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey CollectionType.MultiCollection => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
MultiCollectionId = item.MultiCollectionId, MultiCollectionId = item.MultiCollectionId,
FakeCollectionKey = item.FakeCollectionKey FakeCollectionKey = item.FakeCollectionKey
}, },
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey CollectionType.SmartCollection => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
SmartCollectionId = item.SmartCollectionId, SmartCollectionId = item.SmartCollectionId,
FakeCollectionKey = item.FakeCollectionKey FakeCollectionKey = item.FakeCollectionKey
}, },
ProgramScheduleItemCollectionType.Playlist => new CollectionKey CollectionType.RerunFirstRun => new CollectionKey
{
CollectionType = item.CollectionType,
RerunCollectionId = item.RerunCollectionId,
FakeCollectionKey = item.FakeCollectionKey
},
CollectionType.RerunRerun => new CollectionKey
{
CollectionType = item.CollectionType,
RerunCollectionId = item.RerunCollectionId,
FakeCollectionKey = item.FakeCollectionKey
},
CollectionType.Playlist => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
PlaylistId = item.PlaylistId, PlaylistId = item.PlaylistId,
FakeCollectionKey = item.FakeCollectionKey FakeCollectionKey = item.FakeCollectionKey
}, },
ProgramScheduleItemCollectionType.FakeCollection => new CollectionKey CollectionType.FakeCollection => new CollectionKey
{ {
CollectionType = item.CollectionType, CollectionType = item.CollectionType,
FakeCollectionKey = item.FakeCollectionKey FakeCollectionKey = item.FakeCollectionKey
@ -285,37 +298,37 @@ public class CollectionKey : Record<CollectionKey>
public static CollectionKey ForFillerPreset(FillerPreset filler) => public static CollectionKey ForFillerPreset(FillerPreset filler) =>
filler.CollectionType switch filler.CollectionType switch
{ {
ProgramScheduleItemCollectionType.Collection => new CollectionKey CollectionType.Collection => new CollectionKey
{ {
CollectionType = filler.CollectionType, CollectionType = filler.CollectionType,
CollectionId = filler.CollectionId CollectionId = filler.CollectionId
}, },
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey CollectionType.TelevisionShow => new CollectionKey
{ {
CollectionType = filler.CollectionType, CollectionType = filler.CollectionType,
MediaItemId = filler.MediaItemId MediaItemId = filler.MediaItemId
}, },
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey CollectionType.TelevisionSeason => new CollectionKey
{ {
CollectionType = filler.CollectionType, CollectionType = filler.CollectionType,
MediaItemId = filler.MediaItemId MediaItemId = filler.MediaItemId
}, },
ProgramScheduleItemCollectionType.Artist => new CollectionKey CollectionType.Artist => new CollectionKey
{ {
CollectionType = filler.CollectionType, CollectionType = filler.CollectionType,
MediaItemId = filler.MediaItemId MediaItemId = filler.MediaItemId
}, },
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey CollectionType.MultiCollection => new CollectionKey
{ {
CollectionType = filler.CollectionType, CollectionType = filler.CollectionType,
MultiCollectionId = filler.MultiCollectionId MultiCollectionId = filler.MultiCollectionId
}, },
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey CollectionType.SmartCollection => new CollectionKey
{ {
CollectionType = filler.CollectionType, CollectionType = filler.CollectionType,
SmartCollectionId = filler.SmartCollectionId SmartCollectionId = filler.SmartCollectionId
}, },
ProgramScheduleItemCollectionType.Playlist => new CollectionKey CollectionType.Playlist => new CollectionKey
{ {
CollectionType = filler.CollectionType, CollectionType = filler.CollectionType,
PlaylistId = filler.PlaylistId PlaylistId = filler.PlaylistId

5
ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs

@ -28,7 +28,7 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
State = new CollectionEnumeratorState { Seed = state.Seed }; State = new CollectionEnumeratorState { Seed = state.Seed };
while (State.Index < state.Index) while (State.Index < state.Index)
{ {
MoveNext(); MoveNext(Option<DateTimeOffset>.None);
} }
} }
@ -41,7 +41,8 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
public Option<MediaItem> Current => _sortedMediaItems.Count != 0 ? _sortedMediaItems[State.Index] : None; public Option<MediaItem> Current => _sortedMediaItems.Count != 0 ? _sortedMediaItems[State.Index] : None;
public Option<bool> CurrentIncludeInProgramGuide { get; } public Option<bool> CurrentIncludeInProgramGuide { get; }
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count; public void MoveNext(Option<DateTimeOffset> scheduledAt) =>
State.Index = (State.Index + 1) % _sortedMediaItems.Count;
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value; public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;

20
ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs

@ -138,55 +138,55 @@ public class MarathonHelper(IMediaCollectionRepository mediaCollectionRepository
mediaItem switch mediaItem switch
{ {
Episode e => new GroupKey( Episode e => new GroupKey(
ProgramScheduleItemCollectionType.TelevisionShow, CollectionType.TelevisionShow,
null, null,
null, null,
null, null,
e.Season?.ShowId ?? 0), e.Season?.ShowId ?? 0),
_ => new GroupKey(ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, 0) _ => new GroupKey(CollectionType.TelevisionShow, null, null, null, 0)
}; };
private static GroupKey MediaItemKeyBySeason(MediaItem mediaItem) => private static GroupKey MediaItemKeyBySeason(MediaItem mediaItem) =>
mediaItem switch mediaItem switch
{ {
Episode e => new GroupKey( Episode e => new GroupKey(
ProgramScheduleItemCollectionType.TelevisionSeason, CollectionType.TelevisionSeason,
null, null,
null, null,
null, null,
e.SeasonId), e.SeasonId),
_ => new GroupKey(ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, 0) _ => new GroupKey(CollectionType.TelevisionSeason, null, null, null, 0)
}; };
private static GroupKey MediaItemKeyByArtist(MediaItem mediaItem) => private static GroupKey MediaItemKeyByArtist(MediaItem mediaItem) =>
mediaItem switch mediaItem switch
{ {
MusicVideo mv => new GroupKey( MusicVideo mv => new GroupKey(
ProgramScheduleItemCollectionType.Artist, CollectionType.Artist,
null, null,
null, null,
null, null,
mv.ArtistId), mv.ArtistId),
_ => new GroupKey(ProgramScheduleItemCollectionType.Artist, null, null, null, 0) _ => new GroupKey(CollectionType.Artist, null, null, null, 0)
}; };
private static GroupKey MediaItemKeyByAlbum(MediaItem mediaItem) => private static GroupKey MediaItemKeyByAlbum(MediaItem mediaItem) =>
mediaItem switch mediaItem switch
{ {
Song s => new GroupKey( Song s => new GroupKey(
ProgramScheduleItemCollectionType.Collection, CollectionType.Collection,
s.SongMetadata.HeadOrNone().Map(sm => sm.Album.GetStableHashCode()).IfNone(0), s.SongMetadata.HeadOrNone().Map(sm => sm.Album.GetStableHashCode()).IfNone(0),
null, null,
null, null,
null), null),
MusicVideo mv => new GroupKey( MusicVideo mv => new GroupKey(
ProgramScheduleItemCollectionType.Collection, CollectionType.Collection,
mv.MusicVideoMetadata.HeadOrNone() mv.MusicVideoMetadata.HeadOrNone()
.Map(mvm => $"{mv.ArtistId}-${mvm.Album}".GetStableHashCode()).IfNone(0), .Map(mvm => $"{mv.ArtistId}-${mvm.Album}".GetStableHashCode()).IfNone(0),
null, null,
null, null,
null), null),
_ => new GroupKey(ProgramScheduleItemCollectionType.Collection, 0, null, null, null) _ => new GroupKey(CollectionType.Collection, 0, null, null, null)
}; };
private static PlaylistItem GroupToPlaylistItem( private static PlaylistItem GroupToPlaylistItem(
@ -211,7 +211,7 @@ public class MarathonHelper(IMediaCollectionRepository mediaCollectionRepository
}; };
private record GroupKey( private record GroupKey(
ProgramScheduleItemCollectionType CollectionType, CollectionType CollectionType,
int? CollectionId, int? CollectionId,
int? MultiCollectionId, int? MultiCollectionId,
int? SmartCollectionId, int? SmartCollectionId,

2
ErsatzTV.Core/Scheduling/Engine/PlaylistHelper.cs

@ -38,7 +38,7 @@ public class PlaylistHelper(IMediaCollectionRepository mediaCollectionRepository
{ {
Index = playlistIndex, Index = playlistIndex,
CollectionType = ProgramScheduleItemCollectionType.FakePlaylistItem, CollectionType = CollectionType.FakePlaylistItem,
CollectionId = playlistIndex, CollectionId = playlistIndex,
PlayAll = false, PlayAll = false,

16
ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs

@ -779,13 +779,13 @@ public class SchedulingEngine(
remainingToFill -= itemDuration; remainingToFill -= itemDuration;
_state.CurrentTime += itemDuration; _state.CurrentTime += itemDuration;
enumeratorDetails.Enumerator.MoveNext(); enumeratorDetails.Enumerator.MoveNext(playoutItem.StartOffset);
} }
else if (discardAttempts > 0) else if (discardAttempts > 0)
{ {
// item won't fit; try the next one // item won't fit; try the next one
discardAttempts--; discardAttempts--;
enumeratorDetails.Enumerator.MoveNext(); enumeratorDetails.Enumerator.MoveNext(Option<DateTimeOffset>.None);
} }
else if (trim) else if (trim)
{ {
@ -806,7 +806,7 @@ public class SchedulingEngine(
remainingToFill = TimeSpan.Zero; remainingToFill = TimeSpan.Zero;
_state.CurrentTime = targetTime; _state.CurrentTime = targetTime;
enumeratorDetails.Enumerator.MoveNext(); enumeratorDetails.Enumerator.MoveNext(playoutItem.StartOffset);
} }
else if (maybeFallbackEnumeratorDetails.IsSome) else if (maybeFallbackEnumeratorDetails.IsSome)
{ {
@ -836,7 +836,7 @@ public class SchedulingEngine(
_state.AddedHistory.Add(history); _state.AddedHistory.Add(history);
} }
fallbackEnumeratorDetails.Enumerator.MoveNext(); fallbackEnumeratorDetails.Enumerator.MoveNext(playoutItem.StartOffset);
} }
} }
} }
@ -959,7 +959,7 @@ public class SchedulingEngine(
_state.AddedHistory.Add(history); _state.AddedHistory.Add(history);
} }
enumeratorDetails.Enumerator.MoveNext(); enumeratorDetails.Enumerator.MoveNext(playoutItem.StartOffset);
result = true; result = true;
} }
@ -1077,7 +1077,7 @@ public class SchedulingEngine(
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
enumeratorDetails.Enumerator.MoveNext(); enumeratorDetails.Enumerator.MoveNext(Option<DateTimeOffset>.None);
} }
} }
@ -1115,7 +1115,7 @@ public class SchedulingEngine(
} }
} }
enumeratorDetails.Enumerator.MoveNext(); enumeratorDetails.Enumerator.MoveNext(Option<DateTimeOffset>.None);
} }
} }
} }
@ -1312,7 +1312,7 @@ public class SchedulingEngine(
// only move next at the end, because that may also move // only move next at the end, because that may also move
// the enumerator index // the enumerator index
playlistEnumerator.MoveNext(); playlistEnumerator.MoveNext(Option<DateTimeOffset>.None);
} }
} }

2
ErsatzTV.Core/Scheduling/HistoryDetails.cs

@ -210,7 +210,7 @@ internal static class HistoryDetails
if (!current) if (!current)
{ {
enumerator.MoveNext(); enumerator.MoveNext(Option<DateTimeOffset>.None);
} }
} }
} }

34
ErsatzTV.Core/Scheduling/MediaItemsForCollection.cs

@ -18,77 +18,83 @@ public static class MediaItemsForCollection
switch (collectionKey.CollectionType) switch (collectionKey.CollectionType)
{ {
case ProgramScheduleItemCollectionType.Collection: case CollectionType.Collection:
result.AddRange(await mediaCollectionRepository.GetItems(collectionKey.CollectionId ?? 0)); result.AddRange(await mediaCollectionRepository.GetItems(collectionKey.CollectionId ?? 0));
break; break;
case ProgramScheduleItemCollectionType.TelevisionShow: case CollectionType.TelevisionShow:
result.AddRange(await televisionRepository.GetShowItems(collectionKey.MediaItemId ?? 0)); result.AddRange(await televisionRepository.GetShowItems(collectionKey.MediaItemId ?? 0));
break; break;
case ProgramScheduleItemCollectionType.TelevisionSeason: case CollectionType.TelevisionSeason:
result.AddRange(await televisionRepository.GetSeasonItems(collectionKey.MediaItemId ?? 0)); result.AddRange(await televisionRepository.GetSeasonItems(collectionKey.MediaItemId ?? 0));
break; break;
case ProgramScheduleItemCollectionType.Artist: case CollectionType.Artist:
result.AddRange(await artistRepository.GetArtistItems(collectionKey.MediaItemId ?? 0)); result.AddRange(await artistRepository.GetArtistItems(collectionKey.MediaItemId ?? 0));
break; break;
case ProgramScheduleItemCollectionType.MultiCollection: case CollectionType.MultiCollection:
result.AddRange( result.AddRange(
await mediaCollectionRepository.GetMultiCollectionItems( await mediaCollectionRepository.GetMultiCollectionItems(
collectionKey.MultiCollectionId ?? 0, collectionKey.MultiCollectionId ?? 0,
cancellationToken)); cancellationToken));
break; break;
case ProgramScheduleItemCollectionType.SmartCollection: case CollectionType.SmartCollection:
result.AddRange( result.AddRange(
await mediaCollectionRepository.GetSmartCollectionItems( await mediaCollectionRepository.GetSmartCollectionItems(
collectionKey.SmartCollectionId ?? 0, collectionKey.SmartCollectionId ?? 0,
cancellationToken)); cancellationToken));
break; break;
case ProgramScheduleItemCollectionType.Playlist: case CollectionType.RerunFirstRun or CollectionType.RerunRerun:
result.AddRange(
await mediaCollectionRepository.GetRerunCollectionItems(
collectionKey.RerunCollectionId ?? 0,
cancellationToken));
break;
case CollectionType.Playlist:
result.AddRange( result.AddRange(
await mediaCollectionRepository.GetPlaylistItems(collectionKey.PlaylistId ?? 0, cancellationToken)); await mediaCollectionRepository.GetPlaylistItems(collectionKey.PlaylistId ?? 0, cancellationToken));
break; break;
case ProgramScheduleItemCollectionType.Movie: case CollectionType.Movie:
foreach (int mediaItemId in Optional(collectionKey.MediaItemId)) foreach (int mediaItemId in Optional(collectionKey.MediaItemId))
{ {
result.AddRange(await mediaCollectionRepository.GetMovie(mediaItemId)); result.AddRange(await mediaCollectionRepository.GetMovie(mediaItemId));
} }
break; break;
case ProgramScheduleItemCollectionType.Episode: case CollectionType.Episode:
foreach (int mediaItemId in Optional(collectionKey.MediaItemId)) foreach (int mediaItemId in Optional(collectionKey.MediaItemId))
{ {
result.AddRange(await mediaCollectionRepository.GetEpisode(mediaItemId)); result.AddRange(await mediaCollectionRepository.GetEpisode(mediaItemId));
} }
break; break;
case ProgramScheduleItemCollectionType.MusicVideo: case CollectionType.MusicVideo:
foreach (int mediaItemId in Optional(collectionKey.MediaItemId)) foreach (int mediaItemId in Optional(collectionKey.MediaItemId))
{ {
result.AddRange(await mediaCollectionRepository.GetMusicVideo(mediaItemId)); result.AddRange(await mediaCollectionRepository.GetMusicVideo(mediaItemId));
} }
break; break;
case ProgramScheduleItemCollectionType.OtherVideo: case CollectionType.OtherVideo:
foreach (int mediaItemId in Optional(collectionKey.MediaItemId)) foreach (int mediaItemId in Optional(collectionKey.MediaItemId))
{ {
result.AddRange(await mediaCollectionRepository.GetOtherVideo(mediaItemId)); result.AddRange(await mediaCollectionRepository.GetOtherVideo(mediaItemId));
} }
break; break;
case ProgramScheduleItemCollectionType.Song: case CollectionType.Song:
foreach (int mediaItemId in Optional(collectionKey.MediaItemId)) foreach (int mediaItemId in Optional(collectionKey.MediaItemId))
{ {
result.AddRange(await mediaCollectionRepository.GetSong(mediaItemId)); result.AddRange(await mediaCollectionRepository.GetSong(mediaItemId));
} }
break; break;
case ProgramScheduleItemCollectionType.Image: case CollectionType.Image:
foreach (int mediaItemId in Optional(collectionKey.MediaItemId)) foreach (int mediaItemId in Optional(collectionKey.MediaItemId))
{ {
result.AddRange(await mediaCollectionRepository.GetImage(mediaItemId)); result.AddRange(await mediaCollectionRepository.GetImage(mediaItemId));
} }
break; break;
case ProgramScheduleItemCollectionType.RemoteStream: case CollectionType.RemoteStream:
foreach (int mediaItemId in Optional(collectionKey.MediaItemId)) foreach (int mediaItemId in Optional(collectionKey.MediaItemId))
{ {
result.AddRange(await mediaCollectionRepository.GetRemoteStream(mediaItemId)); result.AddRange(await mediaCollectionRepository.GetRemoteStream(mediaItemId));

6
ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs

@ -58,14 +58,14 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
public Option<TimeSpan> MinimumDuration { get; private set; } public Option<TimeSpan> MinimumDuration { get; private set; }
public void MoveNext() public void MoveNext(Option<DateTimeOffset> scheduledAt)
{ {
foreach (MediaItem maybeMediaItem in _sortedEnumerators[EnumeratorIndex].Enumerator.Current) foreach (MediaItem maybeMediaItem in _sortedEnumerators[EnumeratorIndex].Enumerator.Current)
{ {
_remainingMediaItemIds.Remove(maybeMediaItem.Id); _remainingMediaItemIds.Remove(maybeMediaItem.Id);
} }
_sortedEnumerators[EnumeratorIndex].Enumerator.MoveNext(); _sortedEnumerators[EnumeratorIndex].Enumerator.MoveNext(scheduledAt);
_itemsTakenFromCurrent++; _itemsTakenFromCurrent++;
bool shouldSwitchEnumerator = _batchSize.Match( bool shouldSwitchEnumerator = _batchSize.Match(
@ -238,7 +238,7 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
while (result.State.Index < state.Index) while (result.State.Index < state.Index)
{ {
result.MoveNext(); result.MoveNext(Option<DateTimeOffset>.None);
// previous state is no longer valid; playlist now has fewer items // previous state is no longer valid; playlist now has fewer items
if (result.State.Index == 0) if (result.State.Index == 0)

4
ErsatzTV.Core/Scheduling/PlayoutBuildResult.cs

@ -11,6 +11,8 @@ public record PlayoutBuildResult(
System.Collections.Generic.HashSet<int> ItemsToRemove, System.Collections.Generic.HashSet<int> ItemsToRemove,
List<PlayoutHistory> AddedHistory, List<PlayoutHistory> AddedHistory,
System.Collections.Generic.HashSet<int> HistoryToRemove, System.Collections.Generic.HashSet<int> HistoryToRemove,
List<RerunHistory> AddedRerunHistory,
System.Collections.Generic.HashSet<int> RerunHistoryToRemove,
Option<DateTimeOffset> TimeShiftTo) Option<DateTimeOffset> TimeShiftTo)
{ {
public static PlayoutBuildResult Empty => public static PlayoutBuildResult Empty =>
@ -22,5 +24,7 @@ public record PlayoutBuildResult(
[], [],
[], [],
[], [],
[],
[],
Option<DateTimeOffset>.None); Option<DateTimeOffset>.None);
} }

41
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -21,6 +21,7 @@ public class PlayoutBuilder : IPlayoutBuilder
private readonly IArtistRepository _artistRepository; private readonly IArtistRepository _artistRepository;
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly IRerunHelper _rerunHelper;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMultiEpisodeShuffleCollectionEnumeratorFactory _multiEpisodeFactory; private readonly IMultiEpisodeShuffleCollectionEnumeratorFactory _multiEpisodeFactory;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
@ -34,6 +35,7 @@ public class PlayoutBuilder : IPlayoutBuilder
IArtistRepository artistRepository, IArtistRepository artistRepository,
IMultiEpisodeShuffleCollectionEnumeratorFactory multiEpisodeFactory, IMultiEpisodeShuffleCollectionEnumeratorFactory multiEpisodeFactory,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
IRerunHelper rerunHelper,
ILogger<PlayoutBuilder> logger) ILogger<PlayoutBuilder> logger)
{ {
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
@ -42,6 +44,7 @@ public class PlayoutBuilder : IPlayoutBuilder
_artistRepository = artistRepository; _artistRepository = artistRepository;
_multiEpisodeFactory = multiEpisodeFactory; _multiEpisodeFactory = multiEpisodeFactory;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_rerunHelper = rerunHelper;
_logger = logger; _logger = logger;
} }
@ -89,6 +92,12 @@ public class PlayoutBuilder : IPlayoutBuilder
// } // }
result = await Build(playout, referenceData, result, mode, parameters, cancellationToken); result = await Build(playout, referenceData, result, mode, parameters, cancellationToken);
result = result with
{
RerunHistoryToRemove = _rerunHelper.GetHistoryToRemove(),
AddedRerunHistory = _rerunHelper.GetHistoryToAdd()
};
} }
return result; return result;
@ -181,6 +190,9 @@ public class PlayoutBuilder : IPlayoutBuilder
var smartCollectionIds = var smartCollectionIds =
playout.ProgramScheduleAnchors.Map(a => Optional(a.SmartCollectionId)).Somes().ToHashSet(); playout.ProgramScheduleAnchors.Map(a => Optional(a.SmartCollectionId)).Somes().ToHashSet();
var rerunCollectionIds =
playout.ProgramScheduleAnchors.Map(a => Optional(a.RerunCollectionId)).Somes().ToHashSet();
var mediaItemIds = playout.ProgramScheduleAnchors.Map(a => Optional(a.MediaItemId)).Somes().ToHashSet(); var mediaItemIds = playout.ProgramScheduleAnchors.Map(a => Optional(a.MediaItemId)).Somes().ToHashSet();
playout.ProgramScheduleAnchors.Clear(); playout.ProgramScheduleAnchors.Clear();
@ -206,6 +218,13 @@ public class PlayoutBuilder : IPlayoutBuilder
playout.ProgramScheduleAnchors.Add(minAnchor); playout.ProgramScheduleAnchors.Add(minAnchor);
} }
foreach (int rerunCollectionId in rerunCollectionIds)
{
PlayoutProgramScheduleAnchor minAnchor = allAnchors.Filter(a => a.RerunCollectionId == rerunCollectionId)
.MinBy(a => a.AnchorDateOffset.IfNone(DateTimeOffset.MaxValue).Ticks);
playout.ProgramScheduleAnchors.Add(minAnchor);
}
foreach (int mediaItemId in mediaItemIds) foreach (int mediaItemId in mediaItemIds)
{ {
PlayoutProgramScheduleAnchor minAnchor = allAnchors.Filter(a => a.MediaItemId == mediaItemId) PlayoutProgramScheduleAnchor minAnchor = allAnchors.Filter(a => a.MediaItemId == mediaItemId)
@ -261,6 +280,7 @@ public class PlayoutBuilder : IPlayoutBuilder
referenceData.Channel.Name); referenceData.Channel.Name);
result = result with { ClearItems = true }; result = result with { ClearItems = true };
_rerunHelper.ClearHistory = true;
playout.Anchor = null; playout.Anchor = null;
playout.ProgramScheduleAnchors.Clear(); playout.ProgramScheduleAnchors.Clear();
playout.OnDemandCheckpoint = null; playout.OnDemandCheckpoint = null;
@ -610,19 +630,19 @@ public class PlayoutBuilder : IPlayoutBuilder
{ {
var (showId, _, _) when showId > 0 => new CollectionKey var (showId, _, _) when showId > 0 => new CollectionKey
{ {
CollectionType = ProgramScheduleItemCollectionType.TelevisionShow, CollectionType = CollectionType.TelevisionShow,
MediaItemId = showId, MediaItemId = showId,
FakeCollectionKey = collectionKeyString FakeCollectionKey = collectionKeyString
}, },
var (_, artistId, _) when artistId > 0 => new CollectionKey var (_, artistId, _) when artistId > 0 => new CollectionKey
{ {
CollectionType = ProgramScheduleItemCollectionType.Artist, CollectionType = CollectionType.Artist,
MediaItemId = artistId, MediaItemId = artistId,
FakeCollectionKey = collectionKeyString FakeCollectionKey = collectionKeyString
}, },
var (_, _, k) when k is not null => new CollectionKey var (_, _, k) when k is not null => new CollectionKey
{ {
CollectionType = ProgramScheduleItemCollectionType.FakeCollection, CollectionType = CollectionType.FakeCollection,
FakeCollectionKey = collectionKeyString FakeCollectionKey = collectionKeyString
}, },
var (_, _, _) => null var (_, _, _) => null
@ -1145,6 +1165,7 @@ public class PlayoutBuilder : IPlayoutBuilder
&& a.MediaItemId == collectionKey.MediaItemId && a.MediaItemId == collectionKey.MediaItemId
&& a.FakeCollectionKey == collectionKey.FakeCollectionKey && a.FakeCollectionKey == collectionKey.FakeCollectionKey
&& a.SmartCollectionId == collectionKey.SmartCollectionId && a.SmartCollectionId == collectionKey.SmartCollectionId
&& a.RerunCollectionId == collectionKey.RerunCollectionId
&& a.MultiCollectionId == collectionKey.MultiCollectionId && a.MultiCollectionId == collectionKey.MultiCollectionId
&& a.PlaylistId == collectionKey.PlaylistId && a.PlaylistId == collectionKey.PlaylistId
&& a.AnchorDate is null); && a.AnchorDate is null);
@ -1165,6 +1186,7 @@ public class PlayoutBuilder : IPlayoutBuilder
CollectionId = collectionKey.CollectionId, CollectionId = collectionKey.CollectionId,
MultiCollectionId = collectionKey.MultiCollectionId, MultiCollectionId = collectionKey.MultiCollectionId,
SmartCollectionId = collectionKey.SmartCollectionId, SmartCollectionId = collectionKey.SmartCollectionId,
RerunCollectionId = collectionKey.RerunCollectionId,
MediaItemId = collectionKey.MediaItemId, MediaItemId = collectionKey.MediaItemId,
PlaylistId = collectionKey.PlaylistId, PlaylistId = collectionKey.PlaylistId,
FakeCollectionKey = collectionKey.FakeCollectionKey, FakeCollectionKey = collectionKey.FakeCollectionKey,
@ -1207,6 +1229,7 @@ public class PlayoutBuilder : IPlayoutBuilder
&& a.CollectionId == collectionKey.CollectionId && a.CollectionId == collectionKey.CollectionId
&& a.MultiCollectionId == collectionKey.MultiCollectionId && a.MultiCollectionId == collectionKey.MultiCollectionId
&& a.SmartCollectionId == collectionKey.SmartCollectionId && a.SmartCollectionId == collectionKey.SmartCollectionId
&& a.RerunCollectionId == collectionKey.RerunCollectionId
&& a.MediaItemId == collectionKey.MediaItemId && a.MediaItemId == collectionKey.MediaItemId
&& a.PlaylistId == collectionKey.PlaylistId); && a.PlaylistId == collectionKey.PlaylistId);
@ -1223,7 +1246,13 @@ public class PlayoutBuilder : IPlayoutBuilder
state ??= new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 }; state ??= new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 };
if (collectionKey.CollectionType is ProgramScheduleItemCollectionType.Playlist) if (collectionKey.CollectionType is CollectionType.RerunFirstRun or CollectionType.RerunRerun)
{
await _rerunHelper.InitWithMediaItems(playout.Id, collectionKey, mediaItems, cancellationToken);
return _rerunHelper.CreateEnumerator(collectionKey, state, cancellationToken);
}
if (collectionKey.CollectionType is CollectionType.Playlist)
{ {
foreach (int playlistId in Optional(collectionKey.PlaylistId)) foreach (int playlistId in Optional(collectionKey.PlaylistId))
{ {
@ -1243,7 +1272,7 @@ public class PlayoutBuilder : IPlayoutBuilder
int collectionId = collectionKey.CollectionId ?? 0; int collectionId = collectionKey.CollectionId ?? 0;
if (collectionKey.CollectionType == ProgramScheduleItemCollectionType.Collection && if (collectionKey.CollectionType == CollectionType.Collection &&
await _mediaCollectionRepository.IsCustomPlaybackOrder(collectionId)) await _mediaCollectionRepository.IsCustomPlaybackOrder(collectionId))
{ {
Option<Collection> maybeCollectionWithItems = Option<Collection> maybeCollectionWithItems =
@ -1297,7 +1326,7 @@ public class PlayoutBuilder : IPlayoutBuilder
activeSchedule.RandomStartPoint, activeSchedule.RandomStartPoint,
cancellationToken); cancellationToken);
case PlaybackOrder.MultiEpisodeShuffle when case PlaybackOrder.MultiEpisodeShuffle when
collectionKey.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow && collectionKey.CollectionType == CollectionType.TelevisionShow &&
collectionKey.MediaItemId.HasValue: collectionKey.MediaItemId.HasValue:
foreach (Show show in await _televisionRepository.GetShow(collectionKey.MediaItemId.Value, cancellationToken)) foreach (Show show in await _televisionRepository.GetShow(collectionKey.MediaItemId.Value, cancellationToken))
{ {

22
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs

@ -178,7 +178,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
CurrentTime = nextState.CurrentTime + itemDuration CurrentTime = nextState.CurrentTime + itemDuration
}; };
enumerator.MoveNext(); enumerator.MoveNext(playoutItem.StartOffset);
} }
} }
@ -223,7 +223,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
CurrentTime = nextItemStart.UtcDateTime CurrentTime = nextItemStart.UtcDateTime
}; };
enumerator.MoveNext(); enumerator.MoveNext(playoutItem.StartOffset);
} }
} }
@ -365,9 +365,9 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
} }
// convert playlist filler // convert playlist filler
if (allFiller.Any(f => f.CollectionType is ProgramScheduleItemCollectionType.Playlist)) if (allFiller.Any(f => f.CollectionType is CollectionType.Playlist))
{ {
var toRemove = allFiller.Filter(f => f.CollectionType is ProgramScheduleItemCollectionType.Playlist) var toRemove = allFiller.Filter(f => f.CollectionType is CollectionType.Playlist)
.ToList(); .ToList();
allFiller.RemoveAll(toRemove.Contains); allFiller.RemoveAll(toRemove.Contains);
@ -800,7 +800,9 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
}; };
result.Add(playoutItem); result.Add(playoutItem);
enumerator.MoveNext();
// TODO: this won't work with reruns
enumerator.MoveNext(Option<DateTimeOffset>.None);
} }
} }
@ -845,7 +847,9 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
remainingToFill -= itemDuration; remainingToFill -= itemDuration;
result.Add(playoutItem); result.Add(playoutItem);
enumerator.MoveNext();
// TODO: this won't work with reruns
enumerator.MoveNext(Option<DateTimeOffset>.None);
} }
else else
{ {
@ -857,7 +861,8 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
remainingToFill); remainingToFill);
} }
enumerator.MoveNext(); // TODO: this won't work with reruns
enumerator.MoveNext(Option<DateTimeOffset>.None);
} }
} }
} }
@ -892,7 +897,8 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
DisableWatermarks = !scheduleItem.FallbackFiller.AllowWatermarks DisableWatermarks = !scheduleItem.FallbackFiller.AllowWatermarks
}; };
enumerator.MoveNext(); // TODO: this won't work with reruns
enumerator.MoveNext(Option<DateTimeOffset>.None);
return result; return result;
} }

6
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs

@ -110,7 +110,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
itemDuration, itemDuration,
scheduleItem.PlayoutDuration); scheduleItem.PlayoutDuration);
contentEnumerator.MoveNext(); contentEnumerator.MoveNext(Option<DateTimeOffset>.None);
continue; continue;
} }
@ -137,7 +137,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
itemDuration, itemDuration,
remainingDuration); remainingDuration);
contentEnumerator.MoveNext(); contentEnumerator.MoveNext(Option<DateTimeOffset>.None);
} }
} }
else else
@ -235,7 +235,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
: nextState.NextGuideGroup : nextState.NextGuideGroup
}; };
contentEnumerator.MoveNext(); contentEnumerator.MoveNext(itemStartTime);
} }
else else
{ {

2
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs

@ -145,7 +145,7 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
: nextState.NextGuideGroup : nextState.NextGuideGroup
}; };
contentEnumerator.MoveNext(); contentEnumerator.MoveNext(itemStartTime);
} }
else else
{ {

2
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

@ -156,7 +156,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
: nextState.NextGuideGroup : nextState.NextGuideGroup
}; };
contentEnumerator.MoveNext(); contentEnumerator.MoveNext(playoutItem.StartOffset);
} }
if (nextState.MultipleRemaining.IfNone(-1) == 0) if (nextState.MultipleRemaining.IfNone(-1) == 0)

2
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs

@ -96,7 +96,7 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
}; };
nextState.ScheduleItemsEnumerator.MoveNext(); nextState.ScheduleItemsEnumerator.MoveNext();
contentEnumerator.MoveNext(); contentEnumerator.MoveNext(itemStartTime);
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime); // LogScheduledItem(scheduleItem, mediaItem, itemStartTime);

6
ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs

@ -26,13 +26,13 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator
// because _index defaults to 0 // because _index defaults to 0
if (State.Index == state.Index) if (State.Index == state.Index)
{ {
MoveNext(); MoveNext(Option<DateTimeOffset>.None);
} }
else else
{ {
while (State.Index <= state.Index) while (State.Index <= state.Index)
{ {
MoveNext(); MoveNext(Option<DateTimeOffset>.None);
} }
} }
} }
@ -46,7 +46,7 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator
public Option<MediaItem> Current => _mediaItems.Any() ? _mediaItems[_index] : None; public Option<MediaItem> Current => _mediaItems.Any() ? _mediaItems[_index] : None;
public Option<bool> CurrentIncludeInProgramGuide { get; } public Option<bool> CurrentIncludeInProgramGuide { get; }
public void MoveNext() public void MoveNext(Option<DateTimeOffset> scheduledAt)
{ {
if (_mediaItems.Count == 0) if (_mediaItems.Count == 0)
{ {

6
ErsatzTV.Core/Scheduling/RandomizedRotatingMediaCollectionEnumerator.cs

@ -50,13 +50,13 @@ public class RandomizedRotatingMediaCollectionEnumerator : IMediaCollectionEnume
// because _index defaults to 0 // because _index defaults to 0
if (State.Index == state.Index) if (State.Index == state.Index)
{ {
MoveNext(); MoveNext(Option<DateTimeOffset>.None);
} }
else else
{ {
while (State.Index <= state.Index) while (State.Index <= state.Index)
{ {
MoveNext(); MoveNext(Option<DateTimeOffset>.None);
} }
} }
} }
@ -70,7 +70,7 @@ public class RandomizedRotatingMediaCollectionEnumerator : IMediaCollectionEnume
public Option<MediaItem> Current => _mediaItems.Any() ? _mediaItems[_index] : None; public Option<MediaItem> Current => _mediaItems.Any() ? _mediaItems[_index] : None;
public Option<bool> CurrentIncludeInProgramGuide { get; } public Option<bool> CurrentIncludeInProgramGuide { get; }
public void MoveNext() public void MoveNext(Option<DateTimeOffset> scheduledAt)
{ {
var groups = _groupMedia.Keys.ToList(); var groups = _groupMedia.Keys.ToList();
if (groups.Count == 0) if (groups.Count == 0)

4
ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs

@ -31,7 +31,7 @@ public sealed class SeasonEpisodeMediaCollectionEnumerator : IMediaCollectionEnu
while (State.Index < state.Index) while (State.Index < state.Index)
{ {
MoveNext(); MoveNext(Option<DateTimeOffset>.None);
} }
} }
@ -44,7 +44,7 @@ public sealed class SeasonEpisodeMediaCollectionEnumerator : IMediaCollectionEnu
public Option<MediaItem> Current => _sortedMediaItems.Count != 0 ? _sortedMediaItems[State.Index] : None; public Option<MediaItem> Current => _sortedMediaItems.Count != 0 ? _sortedMediaItems[State.Index] : None;
public Option<bool> CurrentIncludeInProgramGuide { get; } public Option<bool> CurrentIncludeInProgramGuide { get; }
public void MoveNext() public void MoveNext(Option<DateTimeOffset> scheduledAt)
{ {
if (_sortedMediaItems.Count == 0) if (_sortedMediaItems.Count == 0)
{ {

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save