fix bug with smart collection progress (#1118)
This commit is contained in:
@@ -15,7 +15,7 @@ public class FakeMediaCollectionRepository : IMediaCollectionRepository
|
||||
|
||||
public Task<List<MediaItem>> GetItems(int id) => _data[id].ToList().AsTask();
|
||||
public Task<List<MediaItem>> GetMultiCollectionItems(int id) => throw new NotSupportedException();
|
||||
public Task<List<MediaItem>> GetSmartCollectionItems(int id) => throw new NotSupportedException();
|
||||
public Task<List<MediaItem>> GetSmartCollectionItems(int id) => _data[id].ToList().AsTask();
|
||||
|
||||
public Task<List<CollectionWithItems>> GetMultiCollectionCollections(int id) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
@@ -2380,6 +2380,91 @@ public class PlayoutBuilderTests
|
||||
// the continue anchor should have the same seed as the most recent (last) checkpoint from the first run
|
||||
firstSeedValue.Should().Be(secondSeedValue);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed()
|
||||
{
|
||||
var mediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(1, TimeSpan.FromHours(1), DateTime.Today),
|
||||
TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)),
|
||||
TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(3))
|
||||
};
|
||||
|
||||
(PlayoutBuilder builder, Playout playout) =
|
||||
TestDataFloodForSmartCollectionItems(mediaItems, PlaybackOrder.Shuffle);
|
||||
DateTimeOffset start = HoursAfterMidnight(0);
|
||||
DateTimeOffset finish = start + TimeSpan.FromHours(6);
|
||||
|
||||
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
|
||||
|
||||
result.Items.Count.Should().Be(6);
|
||||
result.ProgramScheduleAnchors.Count.Should().Be(2);
|
||||
PlayoutProgramScheduleAnchor primaryAnchor = result.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1);
|
||||
primaryAnchor.EnumeratorState.Seed.Should().BeGreaterThan(0);
|
||||
primaryAnchor.EnumeratorState.Index.Should().Be(0);
|
||||
|
||||
int firstSeedValue = primaryAnchor.EnumeratorState.Seed;
|
||||
|
||||
DateTimeOffset start2 = HoursAfterMidnight(0);
|
||||
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
|
||||
|
||||
Playout result2 = await builder.Build(result, PlayoutBuildMode.Continue, start2, finish2);
|
||||
|
||||
primaryAnchor = result2.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1);
|
||||
int secondSeedValue = primaryAnchor.EnumeratorState.Seed;
|
||||
|
||||
firstSeedValue.Should().Be(secondSeedValue);
|
||||
|
||||
primaryAnchor.EnumeratorState.Index.Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed_MultipleDays()
|
||||
{
|
||||
var mediaItems = new List<MediaItem>();
|
||||
for (int i = 1; i <= 100; i++)
|
||||
{
|
||||
mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i)));
|
||||
}
|
||||
|
||||
(PlayoutBuilder builder, Playout playout) =
|
||||
TestDataFloodForSmartCollectionItems(mediaItems, PlaybackOrder.Shuffle);
|
||||
DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5);
|
||||
DateTimeOffset finish = start + TimeSpan.FromDays(2);
|
||||
|
||||
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
|
||||
|
||||
result.Items.Count.Should().Be(53);
|
||||
result.ProgramScheduleAnchors.Count.Should().Be(4);
|
||||
|
||||
result.ProgramScheduleAnchors.All(x => x.AnchorDate is not null).Should().BeTrue();
|
||||
PlayoutProgramScheduleAnchor lastCheckpoint = result.ProgramScheduleAnchors
|
||||
.Filter(psa => psa.SmartCollectionId == 1)
|
||||
.OrderByDescending(a => a.AnchorDate ?? DateTime.MinValue)
|
||||
.First();
|
||||
lastCheckpoint.EnumeratorState.Seed.Should().BeGreaterThan(0);
|
||||
lastCheckpoint.EnumeratorState.Index.Should().Be(53);
|
||||
|
||||
int firstSeedValue = lastCheckpoint.EnumeratorState.Seed;
|
||||
|
||||
for (var i = 1; i < 20; i++)
|
||||
{
|
||||
DateTimeOffset start2 = start.AddHours(i);
|
||||
DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2);
|
||||
|
||||
Playout result2 = await builder.Build(result, PlayoutBuildMode.Continue, start2, finish2);
|
||||
|
||||
PlayoutProgramScheduleAnchor continueAnchor =
|
||||
result2.ProgramScheduleAnchors
|
||||
.Filter(psa => psa.SmartCollectionId == 1)
|
||||
.First(x => x.AnchorDate is null);
|
||||
int secondSeedValue = continueAnchor.EnumeratorState.Seed;
|
||||
|
||||
// the continue anchor should have the same seed as the most recent (last) checkpoint from the first run
|
||||
firstSeedValue.Should().Be(secondSeedValue);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task FloodContent_Should_FloodWithFixedStartTime_FromAnchor()
|
||||
@@ -2727,10 +2812,34 @@ public class PlayoutBuilderTests
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
CollectionType = ProgramScheduleItemCollectionType.Collection,
|
||||
Collection = mediaCollection,
|
||||
CollectionId = mediaCollection.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = playbackOrder
|
||||
PlaybackOrder = playbackOrder,
|
||||
};
|
||||
|
||||
private static ProgramScheduleItem Flood(
|
||||
SmartCollection smartCollection,
|
||||
SmartCollection fillerCollection,
|
||||
PlaybackOrder playbackOrder) =>
|
||||
new ProgramScheduleItemFlood
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
CollectionType = ProgramScheduleItemCollectionType.SmartCollection,
|
||||
SmartCollection = smartCollection,
|
||||
SmartCollectionId = smartCollection.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = playbackOrder,
|
||||
FallbackFiller = new FillerPreset
|
||||
{
|
||||
Id = 1,
|
||||
CollectionType = ProgramScheduleItemCollectionType.SmartCollection,
|
||||
SmartCollection = fillerCollection,
|
||||
SmartCollectionId = fillerCollection.Id,
|
||||
FillerKind = FillerKind.Fallback
|
||||
}
|
||||
};
|
||||
|
||||
private static Movie TestMovie(int id, TimeSpan duration, DateTime aired) =>
|
||||
@@ -2791,6 +2900,59 @@ public class PlayoutBuilderTests
|
||||
|
||||
return new TestData(builder, playout);
|
||||
}
|
||||
|
||||
private TestData TestDataFloodForSmartCollectionItems(
|
||||
List<MediaItem> mediaItems,
|
||||
PlaybackOrder playbackOrder,
|
||||
Mock<IConfigElementRepository> configMock = null)
|
||||
{
|
||||
var mediaCollection = new SmartCollection
|
||||
{
|
||||
Id = 1,
|
||||
Query = "asdf"
|
||||
};
|
||||
|
||||
var fillerCollection = new SmartCollection
|
||||
{
|
||||
Id = 2,
|
||||
Query = "ghjk"
|
||||
};
|
||||
|
||||
Mock<IConfigElementRepository> configRepo = configMock ?? new Mock<IConfigElementRepository>();
|
||||
|
||||
var collectionRepo = new FakeMediaCollectionRepository(
|
||||
Map(
|
||||
(mediaCollection.Id, mediaItems),
|
||||
(fillerCollection.Id, mediaItems.Take(1).ToList())
|
||||
)
|
||||
);
|
||||
var televisionRepo = new FakeTelevisionRepository();
|
||||
var artistRepo = new Mock<IArtistRepository>();
|
||||
var factory = new Mock<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
var localFileSystem = new Mock<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
configRepo.Object,
|
||||
collectionRepo,
|
||||
televisionRepo,
|
||||
artistRepo.Object,
|
||||
factory.Object,
|
||||
localFileSystem.Object,
|
||||
_logger);
|
||||
|
||||
var items = new List<ProgramScheduleItem> { Flood(mediaCollection, fillerCollection, playbackOrder) };
|
||||
|
||||
var playout = new Playout
|
||||
{
|
||||
Id = 1,
|
||||
ProgramSchedule = new ProgramSchedule { Items = items },
|
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
|
||||
Items = new List<PlayoutItem>(),
|
||||
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
|
||||
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
|
||||
};
|
||||
|
||||
return new TestData(builder, playout);
|
||||
}
|
||||
|
||||
private record TestData(PlayoutBuilder Builder, Playout Playout);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,17 @@ using Dapper;
|
||||
using Destructurama;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Data.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data.Repositories.Caching;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using ErsatzTV.Infrastructure.Search;
|
||||
using LanguageExt.UnsafeValueAccess;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -28,7 +33,7 @@ public class ScheduleIntegrationTests
|
||||
public ScheduleIntegrationTests()
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Information()
|
||||
.MinimumLevel.Debug()
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning)
|
||||
.WriteTo.Console()
|
||||
@@ -37,7 +42,123 @@ public class ScheduleIntegrationTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Test()
|
||||
public async Task TestExistingData()
|
||||
{
|
||||
const string DB_FILE_NAME = "/tmp/whatever.sqlite3";
|
||||
const int PLAYOUT_ID = 39;
|
||||
|
||||
var start = new DateTimeOffset(2023, 1, 18, 11, 0, 0, TimeSpan.FromHours(-5));
|
||||
DateTimeOffset finish = start.AddDays(2);
|
||||
|
||||
IServiceCollection services = new ServiceCollection()
|
||||
.AddLogging();
|
||||
|
||||
var connectionString = $"Data Source={DB_FILE_NAME};foreign keys=true;";
|
||||
|
||||
services.AddDbContext<TvContext>(
|
||||
options => options.UseSqlite(
|
||||
connectionString,
|
||||
o =>
|
||||
{
|
||||
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
|
||||
o.MigrationsAssembly("ErsatzTV.Infrastructure");
|
||||
}),
|
||||
ServiceLifetime.Scoped,
|
||||
ServiceLifetime.Singleton);
|
||||
|
||||
services.AddDbContextFactory<TvContext>(
|
||||
options => options.UseSqlite(
|
||||
connectionString,
|
||||
o =>
|
||||
{
|
||||
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
|
||||
o.MigrationsAssembly("ErsatzTV.Infrastructure");
|
||||
}));
|
||||
|
||||
SqlMapper.AddTypeHandler(new DateTimeOffsetHandler());
|
||||
SqlMapper.AddTypeHandler(new GuidHandler());
|
||||
SqlMapper.AddTypeHandler(new TimeSpanHandler());
|
||||
|
||||
services.AddSingleton((Func<IServiceProvider, ILoggerFactory>)(_ => new SerilogLoggerFactory()));
|
||||
|
||||
services.AddScoped<ISearchRepository, SearchRepository>();
|
||||
services.AddScoped<ICachingSearchRepository, CachingSearchRepository>();
|
||||
services.AddScoped<IConfigElementRepository, ConfigElementRepository>();
|
||||
services.AddScoped<IFallbackMetadataProvider, FallbackMetadataProvider>();
|
||||
|
||||
services.AddSingleton<ISearchIndex, SearchIndex>();
|
||||
|
||||
services.AddSingleton(_ => new Mock<IClient>().Object);
|
||||
|
||||
ServiceProvider provider = services.BuildServiceProvider();
|
||||
|
||||
IDbContextFactory<TvContext> factory = provider.GetRequiredService<IDbContextFactory<TvContext>>();
|
||||
|
||||
ILogger<ScheduleIntegrationTests> logger = provider.GetRequiredService<ILogger<ScheduleIntegrationTests>>();
|
||||
logger.LogInformation("Database is at {File}", DB_FILE_NAME);
|
||||
|
||||
await using TvContext dbContext = await factory.CreateDbContextAsync(CancellationToken.None);
|
||||
await dbContext.Database.MigrateAsync(CancellationToken.None);
|
||||
await DbInitializer.Initialize(dbContext, CancellationToken.None);
|
||||
|
||||
ISearchIndex searchIndex = provider.GetRequiredService<ISearchIndex>();
|
||||
await searchIndex.Initialize(
|
||||
new LocalFileSystem(
|
||||
provider.GetRequiredService<IClient>(),
|
||||
provider.GetRequiredService<ILogger<LocalFileSystem>>()),
|
||||
provider.GetRequiredService<IConfigElementRepository>());
|
||||
|
||||
await searchIndex.Rebuild(
|
||||
provider.GetRequiredService<ICachingSearchRepository>(),
|
||||
provider.GetRequiredService<IFallbackMetadataProvider>());
|
||||
|
||||
var builder = new PlayoutBuilder(
|
||||
new ConfigElementRepository(factory),
|
||||
new MediaCollectionRepository(new Mock<IClient>().Object, searchIndex, factory),
|
||||
new TelevisionRepository(factory),
|
||||
new ArtistRepository(factory),
|
||||
new Mock<IMultiEpisodeShuffleCollectionEnumeratorFactory>().Object,
|
||||
new Mock<ILocalFileSystem>().Object,
|
||||
provider.GetRequiredService<ILogger<PlayoutBuilder>>());
|
||||
|
||||
{
|
||||
await using TvContext context = await factory.CreateDbContextAsync();
|
||||
|
||||
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID);
|
||||
Playout playout = maybePlayout.ValueUnsafe();
|
||||
|
||||
await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
for (var i = 1; i <= (24 * 1); i++)
|
||||
{
|
||||
await using TvContext context = await factory.CreateDbContextAsync();
|
||||
|
||||
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID);
|
||||
Playout playout = maybePlayout.ValueUnsafe();
|
||||
|
||||
await builder.Build(playout, PlayoutBuildMode.Continue, start.AddHours(i), finish.AddHours(i));
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
for (var i = 25; i <= 26; i++)
|
||||
{
|
||||
await using TvContext context = await factory.CreateDbContextAsync();
|
||||
|
||||
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID);
|
||||
Playout playout = maybePlayout.ValueUnsafe();
|
||||
|
||||
await builder.Build(playout, PlayoutBuildMode.Continue, start.AddHours(i), finish.AddHours(i));
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestMockData()
|
||||
{
|
||||
string dbFileName = Path.GetTempFileName() + ".sqlite3";
|
||||
|
||||
@@ -221,8 +342,41 @@ public class ScheduleIntegrationTests
|
||||
return await dbContext.Playouts
|
||||
.Include(p => p.Channel)
|
||||
.Include(p => p.Items)
|
||||
|
||||
.Include(p => p.ProgramScheduleAlternates)
|
||||
.ThenInclude(a => a.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
.ThenInclude(psi => psi.Collection)
|
||||
.Include(p => p.ProgramScheduleAlternates)
|
||||
.ThenInclude(a => a.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
.ThenInclude(psi => psi.MediaItem)
|
||||
.Include(p => p.ProgramScheduleAlternates)
|
||||
.ThenInclude(a => a.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
.ThenInclude(psi => psi.PreRollFiller)
|
||||
.Include(p => p.ProgramScheduleAlternates)
|
||||
.ThenInclude(a => a.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
.ThenInclude(psi => psi.MidRollFiller)
|
||||
.Include(p => p.ProgramScheduleAlternates)
|
||||
.ThenInclude(a => a.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
.ThenInclude(psi => psi.PostRollFiller)
|
||||
.Include(p => p.ProgramScheduleAlternates)
|
||||
.ThenInclude(a => a.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
.ThenInclude(psi => psi.TailFiller)
|
||||
.Include(p => p.ProgramScheduleAlternates)
|
||||
.ThenInclude(a => a.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
.ThenInclude(psi => psi.FallbackFiller)
|
||||
|
||||
.Include(p => p.ProgramScheduleAnchors)
|
||||
.ThenInclude(a => a.EnumeratorState)
|
||||
.Include(p => p.ProgramScheduleAnchors)
|
||||
.ThenInclude(a => a.MediaItem)
|
||||
|
||||
.Include(p => p.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
.ThenInclude(psi => psi.Collection)
|
||||
|
||||
Reference in New Issue
Block a user