Browse Source

add classic schedule marathon (#2400)

pull/2401/head
Jason Dove 11 months ago committed by GitHub
parent
commit
0d69dd58a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      CHANGELOG.md
  2. 45
      ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs
  3. 4
      ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs
  4. 16
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  5. 4
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs
  6. 16
      ErsatzTV.Application/ProgramSchedules/Mapper.cs
  7. 8
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs
  8. 8
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs
  9. 8
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs
  10. 8
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs
  11. 4
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs
  12. 6
      ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs
  13. 10
      ErsatzTV.Core/Domain/MarathonGroupBy.cs
  14. 3
      ErsatzTV.Core/Domain/PlaybackOrder.cs
  15. 4
      ErsatzTV.Core/Domain/ProgramScheduleItem.cs
  16. 59
      ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs
  17. 1
      ErsatzTV.Core/Scheduling/Engine/PlaylistHelper.cs
  18. 3
      ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs
  19. 22
      ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs
  20. 83
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  21. 3
      ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs
  22. 6393
      ErsatzTV.Infrastructure.MySql/Migrations/20250909135040_Add_ScheduleItemMarathon.Designer.cs
  23. 50
      ErsatzTV.Infrastructure.MySql/Migrations/20250909135040_Add_ScheduleItemMarathon.cs
  24. 6396
      ErsatzTV.Infrastructure.MySql/Migrations/20250909144439_Add_ScheduleItemMarathonShuffleItems.Designer.cs
  25. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20250909144439_Add_ScheduleItemMarathonShuffleItems.cs
  26. 12
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  27. 6228
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250909135007_Add_ScheduleItemMarathon.Designer.cs
  28. 50
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250909135007_Add_ScheduleItemMarathon.cs
  29. 6231
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250909144730_Add_ScheduleItemMarathonShuffleItems.Designer.cs
  30. 29
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250909144730_Add_ScheduleItemMarathonShuffleItems.cs
  31. 12
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  32. 2
      ErsatzTV/Pages/Artist.razor
  33. 51
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  34. 2
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  35. 2
      ErsatzTV/Pages/TelevisionSeasonList.razor
  36. 8
      ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

10
CHANGELOG.md

@ -13,6 +13,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `Playout Offset` can be used to offset the times of scheduled playout items from the mirror source channel - `Playout Offset` can be used to offset the times of scheduled playout items from the mirror source channel
- e.g. -2 hours will cause the mirror channel to play content 2 hours before the mirror source channel - e.g. -2 hours will cause the mirror channel to play content 2 hours before the mirror source channel
- Add support for `.aif`, `.aifc`, `.aiff` song files - Add support for `.aif`, `.aifc`, `.aiff` song files
- Classic schedules: add playback order `Marathon`
- This can be used with collections and smart collections
- Items from the collection will be grouped by the `Marathon Group By` setting: `Artist`, `Album`, `Season` or `Show`
- The order of groups can optionally be shuffled
- The order of items in each group can optionally be shuffled (otherwise `Season, Episode` or `Chronological` as appropriate)
- A batch size can be set to limit the number of items to schedule from each group at a time
- Empty or zero batch size means play all items from each group before advancing
- Any other value means play the specified number of items before advancing to the next group
### Fixed ### Fixed
- Fix transcoding content with bt709/pc color metadata - Fix transcoding content with bt709/pc color metadata
@ -2116,7 +2124,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Allow `Shuffle In Order` with Collections and Smart Collections - Allow `Shuffle In Order` with Collections and Smart Collections
- Episodes will be grouped by show, and music videos will be grouped by artist - Episodes will be grouped by show, and music videos will be grouped by artist
- All movies will be a single group (multi-collections are probably better if `Shuffle In Order` is desired for movies) - All movies will be a single group (multi-collections are probably better if `Shuffle In Order` is desired for movies)
- All groups will be be ordered chronologically (custom ordering is only supported in multi-collections) - All groups will be ordered chronologically (custom ordering is only supported in multi-collections)
### Fixed ### Fixed
- Generate XMLTV that validates successfully - Generate XMLTV that validates successfully

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

@ -17,6 +17,10 @@ public record AddProgramScheduleItem(
int? MediaItemId, int? MediaItemId,
int? PlaylistId, int? PlaylistId,
PlaybackOrder PlaybackOrder, PlaybackOrder PlaybackOrder,
MarathonGroupBy MarathonGroupBy,
bool MarathonShuffleGroups,
bool MarathonShuffleItems,
int? MarathonBatchSize,
FillWithGroupMode FillWithGroupMode, FillWithGroupMode FillWithGroupMode,
MultipleMode MultipleMode, MultipleMode MultipleMode,
int? MultipleCount, int? MultipleCount,
@ -36,4 +40,43 @@ public record AddProgramScheduleItem(
string PreferredAudioTitle, string PreferredAudioTitle,
string PreferredSubtitleLanguageCode, string PreferredSubtitleLanguageCode,
ChannelSubtitleMode? SubtitleMode) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, ChannelSubtitleMode? SubtitleMode) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>,
IProgramScheduleItemRequest; IProgramScheduleItemRequest
{
public static AddProgramScheduleItem ForMediaItem(int scheduleId, ProgramScheduleItemCollectionType collectionType, int mediaItemId)
=> new(
scheduleId,
StartType.Dynamic,
StartTime: null,
FixedStartTimeBehavior: null,
PlayoutMode.One,
collectionType,
CollectionId: null,
MultiCollectionId: null,
SmartCollectionId: null,
MediaItemId: mediaItemId,
PlaylistId: null,
PlaybackOrder.Shuffle,
MarathonGroupBy.None,
MarathonShuffleGroups: false,
MarathonShuffleItems: false,
MarathonBatchSize: null,
FillWithGroupMode.None,
MultipleMode.Count,
MultipleCount: null,
PlayoutDuration: null,
TailMode.None,
DiscardToFillAttempts: null,
CustomTitle: null,
GuideMode.Normal,
PreRollFillerId: null,
MidRollFillerId: null,
PostRollFillerId: null,
TailFillerId: null,
FallbackFillerId: null,
WatermarkIds: [],
GraphicsElementIds: [],
PreferredAudioLanguageCode: null,
PreferredAudioTitle: null,
PreferredSubtitleLanguageCode: null,
SubtitleMode: null);
}

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

@ -15,6 +15,10 @@ public interface IProgramScheduleItemRequest
int? PlaylistId { get; } int? PlaylistId { get; }
PlayoutMode PlayoutMode { get; } PlayoutMode PlayoutMode { get; }
PlaybackOrder PlaybackOrder { get; } PlaybackOrder PlaybackOrder { get; }
MarathonGroupBy MarathonGroupBy { get; }
bool MarathonShuffleGroups { get; }
bool MarathonShuffleItems { get; }
int? MarathonBatchSize { get; }
FillWithGroupMode FillWithGroupMode { get; } FillWithGroupMode FillWithGroupMode { get; }
MultipleMode MultipleMode { get; } MultipleMode MultipleMode { get; }
int? MultipleCount { get; } int? MultipleCount { get; }

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

@ -202,6 +202,10 @@ public abstract class ProgramScheduleItemCommandBase
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
PlaylistId = item.PlaylistId, PlaylistId = item.PlaylistId,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,
MarathonGroupBy = item.MarathonGroupBy,
MarathonShuffleGroups = item.MarathonShuffleGroups,
MarathonShuffleItems = item.MarathonShuffleItems,
MarathonBatchSize = item.MarathonBatchSize,
FillWithGroupMode = FillWithGroupMode.None, FillWithGroupMode = FillWithGroupMode.None,
CustomTitle = item.CustomTitle, CustomTitle = item.CustomTitle,
GuideMode = item.GuideMode, GuideMode = item.GuideMode,
@ -228,6 +232,10 @@ public abstract class ProgramScheduleItemCommandBase
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
PlaylistId = item.PlaylistId, PlaylistId = item.PlaylistId,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,
MarathonGroupBy = item.MarathonGroupBy,
MarathonShuffleGroups = item.MarathonShuffleGroups,
MarathonShuffleItems = item.MarathonShuffleItems,
MarathonBatchSize = item.MarathonBatchSize,
FillWithGroupMode = FillWithGroupMode.None, FillWithGroupMode = FillWithGroupMode.None,
CustomTitle = item.CustomTitle, CustomTitle = item.CustomTitle,
GuideMode = item.GuideMode, GuideMode = item.GuideMode,
@ -254,6 +262,10 @@ public abstract class ProgramScheduleItemCommandBase
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
PlaylistId = item.PlaylistId, PlaylistId = item.PlaylistId,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,
MarathonGroupBy = item.MarathonGroupBy,
MarathonShuffleGroups = item.MarathonShuffleGroups,
MarathonShuffleItems = item.MarathonShuffleItems,
MarathonBatchSize = item.MarathonBatchSize,
FillWithGroupMode = item.FillWithGroupMode, FillWithGroupMode = item.FillWithGroupMode,
MultipleMode = item.MultipleMode, MultipleMode = item.MultipleMode,
Count = item.MultipleMode is MultipleMode.Count ? item.MultipleCount.GetValueOrDefault() : 0, Count = item.MultipleMode is MultipleMode.Count ? item.MultipleCount.GetValueOrDefault() : 0,
@ -282,6 +294,10 @@ public abstract class ProgramScheduleItemCommandBase
MediaItemId = item.MediaItemId, MediaItemId = item.MediaItemId,
PlaylistId = item.PlaylistId, PlaylistId = item.PlaylistId,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,
MarathonGroupBy = item.MarathonGroupBy,
MarathonShuffleGroups = item.MarathonShuffleGroups,
MarathonShuffleItems = item.MarathonShuffleItems,
MarathonBatchSize = item.MarathonBatchSize,
FillWithGroupMode = item.FillWithGroupMode, FillWithGroupMode = item.FillWithGroupMode,
PlayoutDuration = item.PlayoutDuration.GetValueOrDefault(), PlayoutDuration = item.PlayoutDuration.GetValueOrDefault(),
TailMode = item.TailMode, TailMode = item.TailMode,

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

@ -17,6 +17,10 @@ public record ReplaceProgramScheduleItem(
int? MediaItemId, int? MediaItemId,
int? PlaylistId, int? PlaylistId,
PlaybackOrder PlaybackOrder, PlaybackOrder PlaybackOrder,
MarathonGroupBy MarathonGroupBy,
bool MarathonShuffleGroups,
bool MarathonShuffleItems,
int? MarathonBatchSize,
FillWithGroupMode FillWithGroupMode, FillWithGroupMode FillWithGroupMode,
MultipleMode MultipleMode, MultipleMode MultipleMode,
int? MultipleCount, int? MultipleCount,

16
ErsatzTV.Application/ProgramSchedules/Mapper.cs

@ -45,6 +45,10 @@ internal static class Mapper
_ => null _ => null
}, },
duration.PlaybackOrder, duration.PlaybackOrder,
duration.MarathonGroupBy,
duration.MarathonShuffleGroups,
duration.MarathonShuffleItems,
duration.MarathonBatchSize,
duration.FillWithGroupMode, duration.FillWithGroupMode,
duration.PlayoutDuration, duration.PlayoutDuration,
duration.TailMode, duration.TailMode,
@ -102,6 +106,10 @@ internal static class Mapper
_ => null _ => null
}, },
flood.PlaybackOrder, flood.PlaybackOrder,
flood.MarathonGroupBy,
flood.MarathonShuffleGroups,
flood.MarathonShuffleItems,
flood.MarathonBatchSize,
flood.FillWithGroupMode, flood.FillWithGroupMode,
flood.CustomTitle, flood.CustomTitle,
flood.GuideMode, flood.GuideMode,
@ -156,6 +164,10 @@ internal static class Mapper
_ => null _ => null
}, },
multiple.PlaybackOrder, multiple.PlaybackOrder,
multiple.MarathonGroupBy,
multiple.MarathonShuffleGroups,
multiple.MarathonShuffleItems,
multiple.MarathonBatchSize,
multiple.FillWithGroupMode, multiple.FillWithGroupMode,
multiple.MultipleMode, multiple.MultipleMode,
multiple.Count, multiple.Count,
@ -212,6 +224,10 @@ internal static class Mapper
_ => null _ => null
}, },
one.PlaybackOrder, one.PlaybackOrder,
one.MarathonGroupBy,
one.MarathonShuffleGroups,
one.MarathonShuffleItems,
one.MarathonBatchSize,
one.FillWithGroupMode, one.FillWithGroupMode,
one.CustomTitle, one.CustomTitle,
one.GuideMode, one.GuideMode,

8
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs

@ -23,6 +23,10 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
PlaylistViewModel playlist, PlaylistViewModel playlist,
NamedMediaItemViewModel mediaItem, NamedMediaItemViewModel mediaItem,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,
MarathonGroupBy marathonGroupBy,
bool marathonShuffleGroups,
bool marathonShuffleItems,
int? marathonBatchSize,
FillWithGroupMode fillWithGroupMode, FillWithGroupMode fillWithGroupMode,
TimeSpan playoutDuration, TimeSpan playoutDuration,
TailMode tailMode, TailMode tailMode,
@ -53,6 +57,10 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
playlist, playlist,
mediaItem, mediaItem,
playbackOrder, playbackOrder,
marathonGroupBy,
marathonShuffleGroups,
marathonShuffleItems,
marathonBatchSize,
fillWithGroupMode, fillWithGroupMode,
customTitle, customTitle,
guideMode, guideMode,

8
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs

@ -23,6 +23,10 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
PlaylistViewModel playlist, PlaylistViewModel playlist,
NamedMediaItemViewModel mediaItem, NamedMediaItemViewModel mediaItem,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,
MarathonGroupBy marathonGroupBy,
bool marathonShuffleGroups,
bool marathonShuffleItems,
int? marathonBatchSize,
FillWithGroupMode fillWithGroupMode, FillWithGroupMode fillWithGroupMode,
string customTitle, string customTitle,
GuideMode guideMode, GuideMode guideMode,
@ -50,6 +54,10 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
playlist, playlist,
mediaItem, mediaItem,
playbackOrder, playbackOrder,
marathonGroupBy,
marathonShuffleGroups,
marathonShuffleItems,
marathonBatchSize,
fillWithGroupMode, fillWithGroupMode,
customTitle, customTitle,
guideMode, guideMode,

8
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs

@ -23,6 +23,10 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
PlaylistViewModel playlist, PlaylistViewModel playlist,
NamedMediaItemViewModel mediaItem, NamedMediaItemViewModel mediaItem,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,
MarathonGroupBy marathonGroupBy,
bool marathonShuffleGroups,
bool marathonShuffleItems,
int? marathonBatchSize,
FillWithGroupMode fillWithGroupMode, FillWithGroupMode fillWithGroupMode,
MultipleMode multipleMode, MultipleMode multipleMode,
int count, int count,
@ -52,6 +56,10 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
playlist, playlist,
mediaItem, mediaItem,
playbackOrder, playbackOrder,
marathonGroupBy,
marathonShuffleGroups,
marathonShuffleItems,
marathonBatchSize,
fillWithGroupMode, fillWithGroupMode,
customTitle, customTitle,
guideMode, guideMode,

8
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs

@ -23,6 +23,10 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
PlaylistViewModel playlist, PlaylistViewModel playlist,
NamedMediaItemViewModel mediaItem, NamedMediaItemViewModel mediaItem,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,
MarathonGroupBy marathonGroupBy,
bool marathonShuffleGroups,
bool marathonShuffleItems,
int? marathonBatchSize,
FillWithGroupMode fillWithGroupMode, FillWithGroupMode fillWithGroupMode,
string customTitle, string customTitle,
GuideMode guideMode, GuideMode guideMode,
@ -50,6 +54,10 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
playlist, playlist,
mediaItem, mediaItem,
playbackOrder, playbackOrder,
marathonGroupBy,
marathonShuffleGroups,
marathonShuffleItems,
marathonBatchSize,
fillWithGroupMode, fillWithGroupMode,
customTitle, customTitle,
guideMode, guideMode,

4
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs

@ -22,6 +22,10 @@ public abstract record ProgramScheduleItemViewModel(
PlaylistViewModel Playlist, PlaylistViewModel Playlist,
NamedMediaItemViewModel MediaItem, NamedMediaItemViewModel MediaItem,
PlaybackOrder PlaybackOrder, PlaybackOrder PlaybackOrder,
MarathonGroupBy MarathonGroupBy,
bool MarathonShuffleGroups,
bool MarathonShuffleItems,
int? MarathonBatchSize,
FillWithGroupMode FillWithGroupMode, FillWithGroupMode FillWithGroupMode,
string CustomTitle, string CustomTitle,
GuideMode GuideMode, GuideMode GuideMode,

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

@ -60,7 +60,8 @@ public class PlaylistEnumeratorTests
repo, repo,
playlistItemMap, playlistItemMap,
new CollectionEnumeratorState(), new CollectionEnumeratorState(),
false, shufflePlaylistItems: false,
batchSize: Option<int>.None,
CancellationToken.None); CancellationToken.None);
var items = new List<int>(); var items = new List<int>();
@ -127,7 +128,8 @@ public class PlaylistEnumeratorTests
repo, repo,
playlistItemMap, playlistItemMap,
new CollectionEnumeratorState(), new CollectionEnumeratorState(),
false, shufflePlaylistItems: false,
batchSize: Option<int>.None,
CancellationToken.None); CancellationToken.None);
var items = new List<int>(); var items = new List<int>();

10
ErsatzTV.Core/Domain/MarathonGroupBy.cs

@ -0,0 +1,10 @@
namespace ErsatzTV.Core.Domain;
public enum MarathonGroupBy
{
None = 0,
Show = 1,
Season = 2,
Artist = 3,
Album = 4
}

3
ErsatzTV.Core/Domain/PlaybackOrder.cs

@ -10,5 +10,6 @@ public enum PlaybackOrder
ShuffleInOrder = 4, ShuffleInOrder = 4,
MultiEpisodeShuffle = 5, MultiEpisodeShuffle = 5,
SeasonEpisode = 6, SeasonEpisode = 6,
RandomRotation = 7 RandomRotation = 7,
Marathon = 8
} }

4
ErsatzTV.Core/Domain/ProgramScheduleItem.cs

@ -31,6 +31,10 @@ public abstract class ProgramScheduleItem
public Playlist Playlist { get; set; } public Playlist Playlist { get; set; }
public string FakeCollectionKey { get; set; } public string FakeCollectionKey { get; set; }
public PlaybackOrder PlaybackOrder { get; set; } public PlaybackOrder PlaybackOrder { get; set; }
public MarathonGroupBy MarathonGroupBy { get; set; }
public bool MarathonShuffleGroups { get; set; }
public bool MarathonShuffleItems { get; set; }
public int? MarathonBatchSize { get; set; }
public int? PreRollFillerId { get; set; } public int? PreRollFillerId { get; set; }
public FillerPreset PreRollFiller { get; set; } public FillerPreset PreRollFiller { get; set; }
public int? MidRollFillerId { get; set; } public int? MidRollFillerId { get; set; }

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

@ -7,6 +7,64 @@ namespace ErsatzTV.Core.Scheduling.Engine;
public class MarathonHelper(IMediaCollectionRepository mediaCollectionRepository) public class MarathonHelper(IMediaCollectionRepository mediaCollectionRepository)
{ {
public async Task<Option<PlaylistEnumerator>> GetEnumerator(
List<MediaItem> mediaItems,
MarathonGroupBy marathonGroupBy,
bool marathonShuffleGroups,
bool marathonShuffleItems,
Option<int> marathonBatchSize,
CollectionEnumeratorState state,
CancellationToken cancellationToken)
{
List<IGrouping<GroupKey, MediaItem>> groups = [];
PlaybackOrder itemPlaybackOrder;
// group by show
switch (marathonGroupBy)
{
case MarathonGroupBy.Show:
groups.AddRange(mediaItems.GroupBy(MediaItemKeyByShow));
itemPlaybackOrder = marathonShuffleItems ? PlaybackOrder.Shuffle : PlaybackOrder.SeasonEpisode;
break;
case MarathonGroupBy.Season:
groups.AddRange(mediaItems.GroupBy(MediaItemKeyBySeason));
itemPlaybackOrder = marathonShuffleItems ? PlaybackOrder.Shuffle : PlaybackOrder.SeasonEpisode;
break;
case MarathonGroupBy.Artist:
groups.AddRange(mediaItems.GroupBy(MediaItemKeyByArtist));
itemPlaybackOrder = marathonShuffleItems ? PlaybackOrder.Shuffle : PlaybackOrder.Chronological;
break;
case MarathonGroupBy.Album:
groups.AddRange(mediaItems.GroupBy(MediaItemKeyByAlbum));
itemPlaybackOrder = marathonShuffleItems ? PlaybackOrder.Shuffle : PlaybackOrder.Chronological;
break;
default:
return Option<PlaylistEnumerator>.None;
}
Dictionary<PlaylistItem, List<MediaItem>> itemMap = [];
for (var index = 0; index < groups.Count; index++)
{
IGrouping<GroupKey, MediaItem> group = groups[index];
PlaylistItem playlistItem = GroupToPlaylistItem(
index,
marathonBatchSize.IsNone,
itemPlaybackOrder,
group);
itemMap.Add(playlistItem, group.ToList());
}
return await PlaylistEnumerator.Create(
mediaCollectionRepository,
itemMap,
state,
marathonShuffleGroups,
marathonBatchSize,
cancellationToken);
}
public async Task<Option<PlaylistContentResult>> GetEnumerator( public async Task<Option<PlaylistContentResult>> GetEnumerator(
Dictionary<string, List<string>> guids, Dictionary<string, List<string>> guids,
List<string> searches, List<string> searches,
@ -68,6 +126,7 @@ public class MarathonHelper(IMediaCollectionRepository mediaCollectionRepository
itemMap, itemMap,
state, state,
shuffleGroups, shuffleGroups,
batchSize: Option<int>.None,
cancellationToken); cancellationToken);
return new PlaylistContentResult( return new PlaylistContentResult(

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

@ -57,6 +57,7 @@ public class PlaylistHelper(IMediaCollectionRepository mediaCollectionRepository
itemMap, itemMap,
state, state,
shufflePlaylistItems: false, shufflePlaylistItems: false,
batchSize: Option<int>.None,
cancellationToken); cancellationToken);
return new PlaylistContentResult( return new PlaylistContentResult(

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

@ -255,7 +255,8 @@ public class SchedulingEngine(
mediaCollectionRepository, mediaCollectionRepository,
itemMap, itemMap,
state, state,
false, shufflePlaylistItems: false,
batchSize: Option<int>.None,
CancellationToken.None); CancellationToken.None);
string historyKey = HistoryDetails.KeyForSchedulingContent(key, PlaybackOrder.None); string historyKey = HistoryDetails.KeyForSchedulingContent(key, PlaybackOrder.None);

22
ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs

@ -15,6 +15,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
private CloneableRandom _random; private CloneableRandom _random;
private bool _shufflePlaylistItems; private bool _shufflePlaylistItems;
private List<IMediaCollectionEnumerator> _sortedEnumerators; private List<IMediaCollectionEnumerator> _sortedEnumerators;
private int _itemsTakenFromCurrent;
private Option<int> _batchSize = Option<int>.None;
private PlaylistEnumerator() private PlaylistEnumerator()
{ {
@ -65,11 +67,18 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
} }
_sortedEnumerators[EnumeratorIndex].MoveNext(); _sortedEnumerators[EnumeratorIndex].MoveNext();
_itemsTakenFromCurrent++;
// if we aren't playing all, or if we just finished playing all, move to the next enumerator bool shouldSwitchEnumerator = _batchSize.Match(
if (!_playAll[EnumeratorIndex] || _sortedEnumerators[EnumeratorIndex].State.Index == 0) // move to the next enumerator if we've hit the batch size
batchSize => _itemsTakenFromCurrent >= batchSize,
// if we aren't playing all, or if we just finished playing all, move to the next enumerator
() => !_playAll[EnumeratorIndex] || _sortedEnumerators[EnumeratorIndex].State.Index == 0);
if (shouldSwitchEnumerator)
{ {
EnumeratorIndex = (EnumeratorIndex + 1) % _sortedEnumerators.Count; EnumeratorIndex = (EnumeratorIndex + 1) % _sortedEnumerators.Count;
_itemsTakenFromCurrent = 0;
} }
State.Index += 1; State.Index += 1;
@ -94,6 +103,7 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
Dictionary<PlaylistItem, List<MediaItem>> playlistItemMap, Dictionary<PlaylistItem, List<MediaItem>> playlistItemMap,
CollectionEnumeratorState state, CollectionEnumeratorState state,
bool shufflePlaylistItems, bool shufflePlaylistItems,
Option<int> batchSize,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = new PlaylistEnumerator var result = new PlaylistEnumerator
@ -101,7 +111,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
_sortedEnumerators = [], _sortedEnumerators = [],
_playAll = [], _playAll = [],
_idsToIncludeInEPG = [], _idsToIncludeInEPG = [],
_shufflePlaylistItems = shufflePlaylistItems _shufflePlaylistItems = shufflePlaylistItems,
_batchSize = batchSize
}; };
// collections should share enumerators // collections should share enumerators
@ -167,6 +178,11 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
case PlaybackOrder.SeasonEpisode: case PlaybackOrder.SeasonEpisode:
// TODO: check random start point? // TODO: check random start point?
enumerator = new SeasonEpisodeMediaCollectionEnumerator(items, initState); enumerator = new SeasonEpisodeMediaCollectionEnumerator(items, initState);
// season, episode will filter out season 0, so we may get an empty enumerator back
if (enumerator.Count == 0)
{
enumerator = null;
}
break; break;
case PlaybackOrder.Random: case PlaybackOrder.Random:
enumerator = new RandomizedMediaCollectionEnumerator(items, initState); enumerator = new RandomizedMediaCollectionEnumerator(items, initState);

83
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -5,11 +5,11 @@ using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.Engine;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json; using Newtonsoft.Json;
using Map = LanguageExt.Map; using Map = LanguageExt.Map;
using Humanizer;
namespace ErsatzTV.Core.Scheduling; namespace ErsatzTV.Core.Scheduling;
@ -530,18 +530,44 @@ public class PlayoutBuilder : IPlayoutBuilder
// use configured playback order for primary collection, shuffle for filler // use configured playback order for primary collection, shuffle for filler
Option<ProgramScheduleItem> maybeScheduleItem = sortedScheduleItems Option<ProgramScheduleItem> maybeScheduleItem = sortedScheduleItems
.FirstOrDefault(item => CollectionKey.ForScheduleItem(item) == collectionKey); .FirstOrDefault(item => CollectionKey.ForScheduleItem(item) == collectionKey);
PlaybackOrder playbackOrder = maybeScheduleItem foreach (var scheduleItem in maybeScheduleItem)
.Match(item => item.PlaybackOrder, () => PlaybackOrder.Shuffle); {
IMediaCollectionEnumerator enumerator = IMediaCollectionEnumerator enumerator =
await GetMediaCollectionEnumerator( await GetMediaCollectionEnumerator(
playout, playout,
activeSchedule, activeSchedule,
collectionKey, collectionKey,
mediaItems, mediaItems,
playbackOrder, scheduleItem.PlaybackOrder,
randomStartPoint, scheduleItem.MarathonGroupBy,
cancellationToken); scheduleItem.MarathonShuffleGroups,
collectionEnumerators.Add(collectionKey, enumerator); scheduleItem.MarathonShuffleItems,
scheduleItem.MarathonBatchSize,
randomStartPoint,
cancellationToken);
collectionEnumerators.Add(collectionKey, enumerator);
}
// filler
if (maybeScheduleItem.IsNone)
{
IMediaCollectionEnumerator enumerator =
await GetMediaCollectionEnumerator(
playout,
activeSchedule,
collectionKey,
mediaItems,
PlaybackOrder.Shuffle,
MarathonGroupBy.None,
marathonShuffleGroups: false,
marathonShuffleItems: false,
marathonBatchSize: null,
randomStartPoint,
cancellationToken);
collectionEnumerators.Add(collectionKey, enumerator);
}
} }
var collectionItemCount = collectionMediaItems.Map((k, v) => (k, v.Count)).Values.ToDictionary(); var collectionItemCount = collectionMediaItems.Map((k, v) => (k, v.Count)).Values.ToDictionary();
@ -618,6 +644,10 @@ public class PlayoutBuilder : IPlayoutBuilder
key, key,
fakeCollection.MediaItems, fakeCollection.MediaItems,
scheduleItem.PlaybackOrder, scheduleItem.PlaybackOrder,
scheduleItem.MarathonGroupBy,
scheduleItem.MarathonShuffleGroups,
scheduleItem.MarathonShuffleItems,
scheduleItem.MarathonBatchSize,
randomStartPoint, randomStartPoint,
cancellationToken); cancellationToken);
@ -1162,6 +1192,10 @@ public class PlayoutBuilder : IPlayoutBuilder
CollectionKey collectionKey, CollectionKey collectionKey,
List<MediaItem> mediaItems, List<MediaItem> mediaItems,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,
MarathonGroupBy marathonGroupBy,
bool marathonShuffleGroups,
bool marathonShuffleItems,
int? marathonBatchSize,
bool randomStartPoint, bool randomStartPoint,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
@ -1199,7 +1233,8 @@ public class PlayoutBuilder : IPlayoutBuilder
_mediaCollectionRepository, _mediaCollectionRepository,
playlistItemMap, playlistItemMap,
state, state,
false, shufflePlaylistItems: false,
batchSize: Option<int>.None,
cancellationToken); cancellationToken);
} }
} }
@ -1304,6 +1339,26 @@ public class PlayoutBuilder : IPlayoutBuilder
cancellationToken), cancellationToken),
state, state,
cancellationToken); cancellationToken);
case PlaybackOrder.Marathon:
var helper = new MarathonHelper(_mediaCollectionRepository);
Option<PlaylistEnumerator> maybeEnumerator = await helper.GetEnumerator(
mediaItems,
marathonGroupBy,
marathonShuffleGroups,
marathonShuffleItems,
marathonBatchSize is > 0 ? marathonBatchSize.Value : Option<int>.None,
state,
cancellationToken);
foreach (var enumerator in maybeEnumerator)
{
return enumerator;
}
// fall through to default case if we can't make the proper enumerator
goto default;
default: default:
// TODO: handle this error case differently? // TODO: handle this error case differently?
return new RandomizedMediaCollectionEnumerator(mediaItems, state); return new RandomizedMediaCollectionEnumerator(mediaItems, state);

3
ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs

@ -173,7 +173,8 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor
mediaCollectionRepository, mediaCollectionRepository,
itemMap, itemMap,
state, state,
false, shufflePlaylistItems: false,
batchSize: Option<int>.None,
cancellationToken); cancellationToken);
} }

6393
ErsatzTV.Infrastructure.MySql/Migrations/20250909135040_Add_ScheduleItemMarathon.Designer.cs generated

File diff suppressed because it is too large Load Diff

50
ErsatzTV.Infrastructure.MySql/Migrations/20250909135040_Add_ScheduleItemMarathon.cs

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

6396
ErsatzTV.Infrastructure.MySql/Migrations/20250909144439_Add_ScheduleItemMarathonShuffleItems.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20250909144439_Add_ScheduleItemMarathonShuffleItems.cs

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

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

@ -2221,6 +2221,18 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int>("Index") b.Property<int>("Index")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int?>("MarathonBatchSize")
.HasColumnType("int");
b.Property<int>("MarathonGroupBy")
.HasColumnType("int");
b.Property<bool>("MarathonShuffleGroups")
.HasColumnType("tinyint(1)");
b.Property<bool>("MarathonShuffleItems")
.HasColumnType("tinyint(1)");
b.Property<int?>("MediaItemId") b.Property<int?>("MediaItemId")
.HasColumnType("int"); .HasColumnType("int");

6228
ErsatzTV.Infrastructure.Sqlite/Migrations/20250909135007_Add_ScheduleItemMarathon.Designer.cs generated

File diff suppressed because it is too large Load Diff

50
ErsatzTV.Infrastructure.Sqlite/Migrations/20250909135007_Add_ScheduleItemMarathon.cs

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

6231
ErsatzTV.Infrastructure.Sqlite/Migrations/20250909144730_Add_ScheduleItemMarathonShuffleItems.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.Sqlite/Migrations/20250909144730_Add_ScheduleItemMarathonShuffleItems.cs

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

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

@ -2114,6 +2114,18 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("Index") b.Property<int>("Index")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int?>("MarathonBatchSize")
.HasColumnType("INTEGER");
b.Property<int>("MarathonGroupBy")
.HasColumnType("INTEGER");
b.Property<bool>("MarathonShuffleGroups")
.HasColumnType("INTEGER");
b.Property<bool>("MarathonShuffleItems")
.HasColumnType("INTEGER");
b.Property<int?>("MediaItemId") b.Property<int?>("MediaItemId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

2
ErsatzTV/Pages/Artist.razor

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

51
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -355,6 +355,7 @@
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem> <MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem>
<MudSelectItem Value="PlaybackOrder.Marathon">Marathon</MudSelectItem>
break; break;
case ProgramScheduleItemCollectionType.TelevisionShow: case ProgramScheduleItemCollectionType.TelevisionShow:
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem> <MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
@ -373,6 +374,44 @@
} }
</MudSelect> </MudSelect>
</MudStack> </MudStack>
@if (_selectedItem.PlaybackOrder is PlaybackOrder.Marathon)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Marathon Group By</MudText>
</div>
<MudSelect @bind-Value="@_selectedItem.MarathonGroupBy" For="@(() => _selectedItem.MarathonGroupBy)">
<MudSelectItem Value="MarathonGroupBy.Album">Album</MudSelectItem>
<MudSelectItem Value="MarathonGroupBy.Artist">Artist</MudSelectItem>
<MudSelectItem Value="MarathonGroupBy.Season">Season</MudSelectItem>
<MudSelectItem Value="MarathonGroupBy.Show">Show</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Marathon Shuffle Groups</MudText>
</div>
<MudCheckBox @bind-Value="_selectedItem.MarathonShuffleGroups"
For="@(() => _selectedItem.MarathonShuffleGroups)"
Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Marathon Shuffle Items</MudText>
</div>
<MudCheckBox @bind-Value="_selectedItem.MarathonShuffleItems"
For="@(() => _selectedItem.MarathonShuffleItems)"
Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Marathon Batch Size</MudText>
</div>
<MudTextField @bind-Value="@_selectedItem.MarathonBatchSize"
For="@(() => _selectedItem.MarathonBatchSize)"
HelperText="How many items to play from each group before advancing; empty or zero will play all items"/>
</MudStack>
}
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"> <div class="d-flex">
<MudText>Playout Mode</MudText> <MudText>Playout Mode</MudText>
@ -802,6 +841,10 @@
Playlist = item.Playlist, Playlist = item.Playlist,
MediaItem = item.MediaItem, MediaItem = item.MediaItem,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,
MarathonGroupBy = item.MarathonGroupBy,
MarathonShuffleGroups = item.MarathonShuffleGroups,
MarathonShuffleItems = item.MarathonShuffleItems,
MarathonBatchSize = item.MarathonBatchSize,
FillWithGroupMode = item.FillWithGroupMode, FillWithGroupMode = item.FillWithGroupMode,
CustomTitle = item.CustomTitle, CustomTitle = item.CustomTitle,
GuideMode = item.GuideMode, GuideMode = item.GuideMode,
@ -873,6 +916,10 @@
Playlist = item.Playlist, Playlist = item.Playlist,
MediaItem = item.MediaItem, MediaItem = item.MediaItem,
PlaybackOrder = item.PlaybackOrder, PlaybackOrder = item.PlaybackOrder,
MarathonGroupBy = item.MarathonGroupBy,
MarathonShuffleGroups = item.MarathonShuffleGroups,
MarathonShuffleItems = item.MarathonShuffleItems,
MarathonBatchSize = item.MarathonBatchSize,
FillWithGroupMode = item.FillWithGroupMode, FillWithGroupMode = item.FillWithGroupMode,
CustomTitle = item.CustomTitle, CustomTitle = item.CustomTitle,
GuideMode = item.GuideMode, GuideMode = item.GuideMode,
@ -943,6 +990,10 @@
item.MediaItem?.MediaItemId, item.MediaItem?.MediaItemId,
item.Playlist?.Id, item.Playlist?.Id,
item.PlaybackOrder, item.PlaybackOrder,
item.MarathonGroupBy,
item.MarathonShuffleGroups,
item.MarathonShuffleItems,
item.MarathonBatchSize,
item.FillWithGroupMode, item.FillWithGroupMode,
item.MultipleMode, item.MultipleMode,
item.MultipleCount, item.MultipleCount,

2
ErsatzTV/Pages/TelevisionEpisodeList.razor

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

2
ErsatzTV/Pages/TelevisionSeasonList.razor

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

8
ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

@ -135,6 +135,14 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
} }
} }
public MarathonGroupBy MarathonGroupBy { get; set; }
public bool MarathonShuffleGroups { get; set; }
public bool MarathonShuffleItems { get; set; }
public int? MarathonBatchSize { get; set; }
public MultipleMode MultipleMode { get; set; } public MultipleMode MultipleMode { get; set; }
public int? MultipleCount public int? MultipleCount

Loading…
Cancel
Save