Adds PlaybackOrder.WeightedShuffle = 9 (Classic engine only) plus the per-source weight it distributes by. Why a new order rather than making ShuffleInOrder weight-aware: ShuffleInOrder's balanced shuffle pads sources to equal length with Option.None spacers, and spacers emit nothing — so one cycle plays every item exactly once and airtime stays proportional to collection size. It prevents *clumping*, not *domination*. Retrofitting weights onto it would silently change shipped users' output. WeightedShuffleCollectionEnumerator picks a source by smooth weighted round-robin (acc += weight; richest wins; pays the total), then takes that source's next item. Weights 3:1 emit A A B A; ties break to the earliest source in list order. Fair-share is the equal-weights default, so one mechanism covers both behaviors in the issue. One rotation is sized so the source needing the most picks works through all its items at its share; smaller sources loop within it — that looping is what makes equal weights mean equal airtime regardless of library size. The sequence stays a pure function of (Seed, Index), so it restores by replay like its siblings and needs no per-source persisted counters. It consumes ShuffleSourceBuilder.GetCollectionItemsForShuffleInOrder unchanged — the schedule-entity-free entry point #380 reserved for this issue. Source identity is destroyed on the Shuffle path (MultiCollectionGrouper collapses to GroupedMediaItem + Distinct), so the weight is only reachable via CollectionWithItems on the ShuffleInOrder-shaped path. Weight is persisted on MultiCollectionItem AND MultiCollectionSmartItem — the mirror is mandatory; omitting the smart item silently un-weights smart-collection members. Both carry a DB default of 1: without it existing rows would migrate to 0, and a 0-weight source is dropped by the enumerator. Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
2.6 KiB
C#
63 lines
2.6 KiB
C#
using ErsatzTV.Core.Domain;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
|
|
namespace ErsatzTV.Infrastructure.Data.Configurations;
|
|
|
|
public class MultiCollectionConfiguration : IEntityTypeConfiguration<MultiCollection>
|
|
{
|
|
public void Configure(EntityTypeBuilder<MultiCollection> builder)
|
|
{
|
|
builder.ToTable("MultiCollection");
|
|
|
|
builder.Property(mc => mc.Version).IsConcurrencyToken();
|
|
|
|
builder.Property(mc => mc.Name)
|
|
.HasMaxLength(50)
|
|
.HasColumnType("varchar(50)");
|
|
|
|
builder.HasIndex(mc => mc.Name)
|
|
.IsUnique();
|
|
|
|
builder.HasMany(m => m.Collections)
|
|
.WithMany(m => m.MultiCollections)
|
|
.UsingEntity<MultiCollectionItem>(
|
|
j => j.HasOne(mci => mci.Collection)
|
|
.WithMany(c => c.MultiCollectionItems)
|
|
.HasForeignKey(mci => mci.CollectionId)
|
|
.OnDelete(DeleteBehavior.Cascade),
|
|
j => j.HasOne(mci => mci.MultiCollection)
|
|
.WithMany(mc => mc.MultiCollectionItems)
|
|
.HasForeignKey(mci => mci.MultiCollectionId)
|
|
.OnDelete(DeleteBehavior.Cascade),
|
|
j =>
|
|
{
|
|
j.HasKey(mci => new { mci.MultiCollectionId, mci.CollectionId });
|
|
|
|
// default 1 = fair share (#70). Must be a DB default too: existing rows would
|
|
// otherwise migrate to 0, and a 0-weight source is dropped by the enumerator.
|
|
j.Property(mci => mci.Weight).HasDefaultValue(1);
|
|
});
|
|
|
|
builder.HasMany(m => m.SmartCollections)
|
|
.WithMany(m => m.MultiCollections)
|
|
.UsingEntity<MultiCollectionSmartItem>(
|
|
j => j.HasOne(mci => mci.SmartCollection)
|
|
.WithMany(c => c.MultiCollectionSmartItems)
|
|
.HasForeignKey(mci => mci.SmartCollectionId)
|
|
.OnDelete(DeleteBehavior.Cascade),
|
|
j => j.HasOne(mci => mci.MultiCollection)
|
|
.WithMany(mc => mc.MultiCollectionSmartItems)
|
|
.HasForeignKey(mci => mci.MultiCollectionId)
|
|
.OnDelete(DeleteBehavior.Cascade),
|
|
j =>
|
|
{
|
|
j.HasKey(mci => new { mci.MultiCollectionId, mci.SmartCollectionId });
|
|
|
|
// mirrors MultiCollectionItem above -- omitting it would silently un-weight
|
|
// every smart-collection member of a multi collection
|
|
j.Property(mci => mci.Weight).HasDefaultValue(1);
|
|
});
|
|
}
|
|
}
|