Files
ersatztv/ErsatzTV.Core/Scheduling/ShuffleSourceBuilder.cs
T
timothyandClaude Opus 4.8 764cd3c31b feat(380): extract shuffle-source construction to ShuffleSourceBuilder
Eliminate the one cross-engine reach-in in the scheduler: PlaylistEnumerator called
PlayoutBuilder.GetGroupedMediaItemsForShuffle / GetCollectionItemsForShuffleInOrder as
statics (one engine reaching into another engine's class). Move both helpers verbatim to
a new public static ShuffleSourceBuilder in ErsatzTV.Core/Scheduling (sibling to the
also-static MultiCollectionGrouper / MultiPartEpisodeGrouper; deps passed as parameters,
not DI). Classic (PlayoutBuilder) and Playlist (PlaylistEnumerator) now share this one
place to build shuffle sources.

One intentional signature change: GetGroupedMediaItemsForShuffle takes
(bool keepMultiPartEpisodesTogether, bool treatCollectionsAsShows) instead of a
ProgramSchedule (verified those are the only two properties it read). This deletes
PlaylistEnumerator's fake `new ProgramSchedule { KeepMultiPartEpisodesTogether = false }`
(its TODO becomes an honest false, false) and gives callers without a ProgramSchedule
(#176, #70) a schedule-entity-free entry point.

Scope is deliberately (a)-only: engine separation preserved, no god-factory. Block stays
its own family; the Scripted/YAML construction duplication is a separate follow-up gated
on #381. See docs/decisions.md 2026-07-17.

Behavior-preserving: the characterization net added in the previous commit (classic-shuffle
golden byte-identical, PlaylistEnumerator reach-in sequence unchanged) plus new direct
ShuffleSourceBuilder unit tests (multi-collection vs fake-multi-collection lookup;
multi-part grouping on/off) all green. Full Core.Tests: 546 passed. PlayoutBuilder.cs
also de-BOM'd + whitespace-normalized per the fix-as-you-touch convention (#311).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 08:48:05 +02:00

67 lines
2.8 KiB
C#

using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Core.Scheduling;
/// <summary>
/// Builds the shuffle "source" for a collection key — the grouped media items fed to
/// <see cref="ShuffledMediaCollectionEnumerator" /> and the per-collection lists fed to
/// <see cref="ShuffleInOrderCollectionEnumerator" />.
/// <para>
/// Extracted from <c>PlayoutBuilder</c> (issue #380) so both the Classic builder and
/// <see cref="PlaylistEnumerator" /> share ONE stateless place to construct shuffle sources,
/// eliminating <see cref="PlaylistEnumerator" />'s cross-engine reach-in into
/// <c>PlayoutBuilder</c>'s statics. Kept as a static helper (dependencies passed as parameters)
/// to match the sibling <see cref="MultiCollectionGrouper" /> / <see cref="MultiPartEpisodeGrouper" />
/// (also <c>public static</c>) and because <see cref="PlaylistEnumerator.Create" /> is itself a
/// static factory.
/// </para>
/// </summary>
public static class ShuffleSourceBuilder
{
public static async Task<List<GroupedMediaItem>> GetGroupedMediaItemsForShuffle(
IMediaCollectionRepository mediaCollectionRepository,
bool keepMultiPartEpisodesTogether,
bool treatCollectionsAsShows,
List<MediaItem> mediaItems,
CollectionKey collectionKey,
CancellationToken cancellationToken)
{
if (collectionKey.MultiCollectionId != null)
{
List<CollectionWithItems> collections = await mediaCollectionRepository
.GetMultiCollectionCollections(collectionKey.MultiCollectionId.Value, cancellationToken);
return MultiCollectionGrouper.GroupMediaItems(collections);
}
return keepMultiPartEpisodesTogether
? MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, treatCollectionsAsShows)
: mediaItems.Map(mi => new GroupedMediaItem(mi, null)).ToList();
}
public static async Task<List<CollectionWithItems>> GetCollectionItemsForShuffleInOrder(
IMediaCollectionRepository mediaCollectionRepository,
CollectionKey collectionKey,
CancellationToken cancellationToken)
{
List<CollectionWithItems> result;
if (collectionKey.MultiCollectionId != null)
{
result = await mediaCollectionRepository.GetMultiCollectionCollections(
collectionKey.MultiCollectionId.Value,
cancellationToken);
}
else
{
result = await mediaCollectionRepository.GetFakeMultiCollectionCollections(
collectionKey.CollectionId,
collectionKey.SmartCollectionId,
cancellationToken);
}
return result;
}
}