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>
67 lines
2.8 KiB
C#
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;
|
|
}
|
|
}
|