511 lines
19 KiB
C#
511 lines
19 KiB
C#
using Dapper;
|
|
using Destructurama;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Core.Domain.Scheduling;
|
|
using ErsatzTV.Core.Interfaces.Metadata;
|
|
using ErsatzTV.Core.Interfaces.Repositories;
|
|
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.Extensions;
|
|
using ErsatzTV.Infrastructure.Metadata;
|
|
using ErsatzTV.Infrastructure.Search;
|
|
using ErsatzTV.Infrastructure.Sqlite.Data;
|
|
using LanguageExt.UnsafeValueAccess;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using NSubstitute;
|
|
using NUnit.Framework;
|
|
using Serilog;
|
|
using Serilog.Events;
|
|
using Serilog.Extensions.Logging;
|
|
using MockFileSystem = Testably.Abstractions.Testing.MockFileSystem;
|
|
|
|
namespace ErsatzTV.Core.Tests.Scheduling;
|
|
|
|
[TestFixture]
|
|
[Explicit]
|
|
public class ScheduleIntegrationTests
|
|
{
|
|
[SetUp]
|
|
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromMinutes(1)).Token;
|
|
|
|
private CancellationToken _cancellationToken;
|
|
|
|
public ScheduleIntegrationTests() =>
|
|
Log.Logger = new LoggerConfiguration()
|
|
.MinimumLevel.Debug()
|
|
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
|
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning)
|
|
.WriteTo.Console()
|
|
.Destructure.UsingAttributes()
|
|
.CreateLogger();
|
|
|
|
[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.Sqlite");
|
|
}),
|
|
ServiceLifetime.Scoped,
|
|
ServiceLifetime.Singleton);
|
|
|
|
services.AddDbContextFactory<TvContext>(options => options.UseSqlite(
|
|
connectionString,
|
|
o =>
|
|
{
|
|
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
|
|
o.MigrationsAssembly("ErsatzTV.Infrastructure.Sqlite");
|
|
}));
|
|
|
|
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<ILanguageCodeService, LanguageCodeService>();
|
|
services.AddScoped<IConfigElementRepository, ConfigElementRepository>();
|
|
services.AddScoped<IFallbackMetadataProvider, FallbackMetadataProvider>();
|
|
|
|
services.AddSingleton<ISearchIndex, LuceneSearchIndex>();
|
|
services.AddSingleton<ILanguageCodeCache, LanguageCodeCache>();
|
|
|
|
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(
|
|
new MockFileSystem(),
|
|
provider.GetRequiredService<ILogger<LocalFileSystem>>()),
|
|
provider.GetRequiredService<IConfigElementRepository>(),
|
|
_cancellationToken);
|
|
|
|
await searchIndex.Rebuild(
|
|
provider.GetRequiredService<ISearchRepository>(),
|
|
provider.GetRequiredService<IFallbackMetadataProvider>(),
|
|
provider.GetRequiredService<ILanguageCodeService>(),
|
|
_cancellationToken);
|
|
|
|
var builder = new PlayoutBuilder(
|
|
new ConfigElementRepository(factory),
|
|
new MediaCollectionRepository(searchIndex, factory),
|
|
new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()),
|
|
new ArtistRepository(factory),
|
|
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(),
|
|
new MockFileSystem(),
|
|
Substitute.For<IRerunHelper>(),
|
|
provider.GetRequiredService<ILogger<PlayoutBuilder>>());
|
|
|
|
{
|
|
await using TvContext context = await factory.CreateDbContextAsync(_cancellationToken);
|
|
|
|
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID, _cancellationToken);
|
|
Playout playout = maybePlayout.ValueUnsafe();
|
|
PlayoutReferenceData referenceData = await GetReferenceData(
|
|
context,
|
|
PLAYOUT_ID,
|
|
PlayoutScheduleKind.Classic);
|
|
|
|
await builder.Build(
|
|
playout,
|
|
referenceData,
|
|
PlayoutBuildResult.Empty,
|
|
PlayoutBuildMode.Reset,
|
|
start,
|
|
finish,
|
|
_cancellationToken);
|
|
|
|
// TODO: would need to apply changes from build result
|
|
await context.SaveChangesAsync(_cancellationToken);
|
|
}
|
|
|
|
for (var i = 1; i <= 24 * 1; i++)
|
|
{
|
|
await using TvContext context = await factory.CreateDbContextAsync(_cancellationToken);
|
|
|
|
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID, _cancellationToken);
|
|
Playout playout = maybePlayout.ValueUnsafe();
|
|
PlayoutReferenceData referenceData = await GetReferenceData(
|
|
context,
|
|
PLAYOUT_ID,
|
|
PlayoutScheduleKind.Classic);
|
|
|
|
await builder.Build(
|
|
playout,
|
|
referenceData,
|
|
PlayoutBuildResult.Empty,
|
|
PlayoutBuildMode.Continue,
|
|
start.AddHours(i),
|
|
finish.AddHours(i),
|
|
_cancellationToken);
|
|
|
|
// TODO: would need to apply changes from build result
|
|
await context.SaveChangesAsync(_cancellationToken);
|
|
}
|
|
|
|
for (var i = 25; i <= 26; i++)
|
|
{
|
|
await using TvContext context = await factory.CreateDbContextAsync(_cancellationToken);
|
|
|
|
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID, _cancellationToken);
|
|
Playout playout = maybePlayout.ValueUnsafe();
|
|
PlayoutReferenceData referenceData = await GetReferenceData(
|
|
context,
|
|
PLAYOUT_ID,
|
|
PlayoutScheduleKind.Classic);
|
|
|
|
await builder.Build(
|
|
playout,
|
|
referenceData,
|
|
PlayoutBuildResult.Empty,
|
|
PlayoutBuildMode.Continue,
|
|
start.AddHours(i),
|
|
finish.AddHours(i),
|
|
_cancellationToken);
|
|
|
|
// TODO: would need to apply changes from build result
|
|
await context.SaveChangesAsync(_cancellationToken);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task TestMockData()
|
|
{
|
|
string dbFileName = Path.GetTempFileName() + ".sqlite3";
|
|
|
|
IServiceCollection services = new ServiceCollection()
|
|
.AddLogging();
|
|
|
|
var connectionString = $"Data Source={dbFileName};foreign keys=true;";
|
|
|
|
services.AddDbContext<TvContext>(
|
|
options => options.UseSqlite(
|
|
connectionString,
|
|
o =>
|
|
{
|
|
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
|
|
o.MigrationsAssembly("ErsatzTV.Infrastructure.Sqlite");
|
|
}),
|
|
ServiceLifetime.Scoped,
|
|
ServiceLifetime.Singleton);
|
|
|
|
services.AddDbContextFactory<TvContext>(options => options.UseSqlite(
|
|
connectionString,
|
|
o =>
|
|
{
|
|
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
|
|
o.MigrationsAssembly("ErsatzTV.Infrastructure.Sqlite");
|
|
}));
|
|
|
|
SqlMapper.AddTypeHandler(new DateTimeOffsetHandler());
|
|
SqlMapper.AddTypeHandler(new GuidHandler());
|
|
SqlMapper.AddTypeHandler(new TimeSpanHandler());
|
|
|
|
services.AddSingleton((Func<IServiceProvider, ILoggerFactory>)(_ => new SerilogLoggerFactory()));
|
|
|
|
ServiceProvider provider = services.BuildServiceProvider();
|
|
|
|
IDbContextFactory<TvContext> factory = provider.GetRequiredService<IDbContextFactory<TvContext>>();
|
|
|
|
ILogger<ScheduleIntegrationTests> logger = provider.GetRequiredService<ILogger<ScheduleIntegrationTests>>();
|
|
logger.LogInformation("Database is at {File}", dbFileName);
|
|
|
|
await using TvContext dbContext = await factory.CreateDbContextAsync(CancellationToken.None);
|
|
await dbContext.Database.MigrateAsync(CancellationToken.None);
|
|
await DbInitializer.Initialize(dbContext, CancellationToken.None);
|
|
|
|
var path = new LibraryPath
|
|
{
|
|
Path = "Test LibraryPath"
|
|
};
|
|
|
|
var library = new LocalLibrary
|
|
{
|
|
MediaKind = LibraryMediaKind.Movies,
|
|
Paths = new List<LibraryPath> { path },
|
|
MediaSource = new LocalMediaSource()
|
|
};
|
|
|
|
await dbContext.Libraries.AddAsync(library, _cancellationToken);
|
|
await dbContext.SaveChangesAsync(_cancellationToken);
|
|
|
|
var movies = new List<Movie>();
|
|
for (var i = 1; i < 25; i++)
|
|
{
|
|
var movie = new Movie
|
|
{
|
|
MediaVersions = new List<MediaVersion>
|
|
{
|
|
new() { Duration = TimeSpan.FromMinutes(55) }
|
|
},
|
|
MovieMetadata = new List<MovieMetadata>
|
|
{
|
|
new()
|
|
{
|
|
Title = $"Movie {i}",
|
|
ReleaseDate = new DateTime(2000, 1, 1).AddDays(i)
|
|
}
|
|
},
|
|
LibraryPath = path,
|
|
LibraryPathId = path.Id
|
|
};
|
|
|
|
movies.Add(movie);
|
|
}
|
|
|
|
await dbContext.Movies.AddRangeAsync(movies, _cancellationToken);
|
|
await dbContext.SaveChangesAsync(_cancellationToken);
|
|
|
|
var collection = new Collection
|
|
{
|
|
Name = "Test Collection",
|
|
MediaItems = movies.Cast<MediaItem>().ToList()
|
|
};
|
|
|
|
await dbContext.Collections.AddAsync(collection, _cancellationToken);
|
|
await dbContext.SaveChangesAsync(_cancellationToken);
|
|
|
|
var scheduleItems = new List<ProgramScheduleItem>
|
|
{
|
|
new ProgramScheduleItemDuration
|
|
{
|
|
Collection = collection,
|
|
CollectionId = collection.Id,
|
|
PlayoutDuration = TimeSpan.FromHours(1),
|
|
TailMode = TailMode.None, // immediately continue
|
|
PlaybackOrder = PlaybackOrder.Shuffle
|
|
}
|
|
};
|
|
|
|
int playoutId = await AddTestData(dbContext, scheduleItems);
|
|
|
|
var start = new DateTimeOffset(2022, 7, 26, 8, 0, 5, TimeSpan.FromHours(-5));
|
|
DateTimeOffset finish = start.AddDays(2);
|
|
|
|
var builder = new PlayoutBuilder(
|
|
new ConfigElementRepository(factory),
|
|
new MediaCollectionRepository(Substitute.For<ISearchIndex>(), factory),
|
|
new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()),
|
|
new ArtistRepository(factory),
|
|
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(),
|
|
new MockFileSystem(),
|
|
Substitute.For<IRerunHelper>(),
|
|
provider.GetRequiredService<ILogger<PlayoutBuilder>>());
|
|
|
|
for (var i = 0; i <= 24 * 4; i++)
|
|
{
|
|
await using TvContext context = await factory.CreateDbContextAsync(_cancellationToken);
|
|
|
|
Option<Playout> maybePlayout = await GetPlayout(context, playoutId, _cancellationToken);
|
|
Playout playout = maybePlayout.ValueUnsafe();
|
|
PlayoutReferenceData referenceData = await GetReferenceData(
|
|
context,
|
|
playoutId,
|
|
PlayoutScheduleKind.Classic);
|
|
|
|
await builder.Build(
|
|
playout,
|
|
referenceData,
|
|
PlayoutBuildResult.Empty,
|
|
PlayoutBuildMode.Continue,
|
|
start.AddHours(i),
|
|
finish.AddHours(i),
|
|
_cancellationToken);
|
|
|
|
// TODO: would need to apply changes from build result
|
|
await context.SaveChangesAsync(_cancellationToken);
|
|
}
|
|
}
|
|
|
|
private static async Task<int> AddTestData(TvContext dbContext, List<ProgramScheduleItem> scheduleItems)
|
|
{
|
|
var ffmpegProfile = new FFmpegProfile
|
|
{
|
|
Name = "Test FFmpeg Profile"
|
|
};
|
|
|
|
await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var channel = new Channel(Guid.Parse("00000000-0000-0000-0000-000000000001"))
|
|
{
|
|
Name = "Test Channel",
|
|
FFmpegProfile = ffmpegProfile,
|
|
FFmpegProfileId = ffmpegProfile.Id
|
|
};
|
|
|
|
await dbContext.Channels.AddAsync(channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var schedule = new ProgramSchedule
|
|
{
|
|
Name = "Test Schedule",
|
|
Items = scheduleItems
|
|
};
|
|
|
|
await dbContext.ProgramSchedules.AddAsync(schedule);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var playout = new Playout
|
|
{
|
|
Channel = channel,
|
|
ChannelId = channel.Id,
|
|
ProgramSchedule = schedule,
|
|
ProgramScheduleId = schedule.Id
|
|
};
|
|
|
|
await dbContext.Playouts.AddAsync(playout);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
return playout.Id;
|
|
}
|
|
|
|
private static async Task<Option<Playout>> GetPlayout(
|
|
TvContext dbContext,
|
|
int playoutId,
|
|
CancellationToken cancellationToken) =>
|
|
await dbContext.Playouts
|
|
.Include(p => p.ProgramScheduleAnchors)
|
|
.ThenInclude(a => a.EnumeratorState)
|
|
.SelectOneAsync(p => p.Id, p => p.Id == playoutId, cancellationToken);
|
|
|
|
private static async Task<PlayoutReferenceData> GetReferenceData(
|
|
TvContext dbContext,
|
|
int playoutId,
|
|
PlayoutScheduleKind scheduleKind)
|
|
{
|
|
Channel channel = await dbContext.Channels
|
|
.AsNoTracking()
|
|
.Where(c => c.Playouts.Any(p => p.Id == playoutId))
|
|
.FirstOrDefaultAsync();
|
|
|
|
List<PlayoutItem> existingItems = [];
|
|
List<PlayoutTemplate> playoutTemplates = [];
|
|
|
|
if (scheduleKind is PlayoutScheduleKind.Block)
|
|
{
|
|
existingItems = await dbContext.PlayoutItems
|
|
.AsNoTracking()
|
|
.Where(pi => pi.PlayoutId == playoutId)
|
|
.ToListAsync();
|
|
|
|
playoutTemplates = await dbContext.PlayoutTemplates
|
|
.AsNoTracking()
|
|
.Where(pt => pt.PlayoutId == playoutId)
|
|
.Include(t => t.Template)
|
|
.ThenInclude(t => t.Items)
|
|
.ThenInclude(i => i.Block)
|
|
.ThenInclude(b => b.Items)
|
|
.Include(t => t.DecoTemplate)
|
|
.ThenInclude(t => t.Items)
|
|
.ThenInclude(i => i.Deco)
|
|
.ToListAsync();
|
|
}
|
|
|
|
ProgramSchedule programSchedule = await dbContext.ProgramSchedules
|
|
.AsNoTracking()
|
|
.Where(ps => ps.Playouts.Any(p => p.Id == playoutId))
|
|
.Include(ps => ps.Items)
|
|
.ThenInclude(psi => psi.ProgramScheduleItemWatermarks)
|
|
.ThenInclude(psi => psi.Watermark)
|
|
.Include(ps => ps.Items)
|
|
.ThenInclude(psi => psi.ProgramScheduleItemGraphicsElements)
|
|
.ThenInclude(psi => psi.GraphicsElement)
|
|
.Include(ps => ps.Items)
|
|
.ThenInclude(psi => psi.Collection)
|
|
.Include(ps => ps.Items)
|
|
.ThenInclude(psi => psi.MediaItem)
|
|
.Include(ps => ps.Items)
|
|
.ThenInclude(psi => psi.PreRollFiller)
|
|
.Include(ps => ps.Items)
|
|
.ThenInclude(psi => psi.MidRollFiller)
|
|
.Include(ps => ps.Items)
|
|
.ThenInclude(psi => psi.PostRollFiller)
|
|
.Include(ps => ps.Items)
|
|
.ThenInclude(psi => psi.TailFiller)
|
|
.Include(ps => ps.Items)
|
|
.ThenInclude(psi => psi.FallbackFiller)
|
|
.FirstOrDefaultAsync();
|
|
|
|
List<ProgramScheduleAlternate> programScheduleAlternates = await dbContext.ProgramScheduleAlternates
|
|
.AsNoTracking()
|
|
.Where(pt => pt.PlayoutId == playoutId)
|
|
.Include(a => a.ProgramSchedule)
|
|
.ThenInclude(ps => ps.Items)
|
|
.ThenInclude(psi => psi.ProgramScheduleItemWatermarks)
|
|
.ThenInclude(psi => psi.Watermark)
|
|
.Include(a => a.ProgramSchedule)
|
|
.ThenInclude(ps => ps.Items)
|
|
.ThenInclude(psi => psi.ProgramScheduleItemGraphicsElements)
|
|
.ThenInclude(psi => psi.GraphicsElement)
|
|
.Include(a => a.ProgramSchedule)
|
|
.ThenInclude(ps => ps.Items)
|
|
.ThenInclude(psi => psi.Collection)
|
|
.Include(a => a.ProgramSchedule)
|
|
.ThenInclude(ps => ps.Items)
|
|
.ThenInclude(psi => psi.MediaItem)
|
|
.Include(a => a.ProgramSchedule)
|
|
.ThenInclude(ps => ps.Items)
|
|
.ThenInclude(psi => psi.PreRollFiller)
|
|
.Include(a => a.ProgramSchedule)
|
|
.ThenInclude(ps => ps.Items)
|
|
.ThenInclude(psi => psi.MidRollFiller)
|
|
.Include(a => a.ProgramSchedule)
|
|
.ThenInclude(ps => ps.Items)
|
|
.ThenInclude(psi => psi.PostRollFiller)
|
|
.Include(a => a.ProgramSchedule)
|
|
.ThenInclude(ps => ps.Items)
|
|
.ThenInclude(psi => psi.TailFiller)
|
|
.Include(a => a.ProgramSchedule)
|
|
.ThenInclude(ps => ps.Items)
|
|
.ThenInclude(psi => psi.FallbackFiller)
|
|
.ToListAsync();
|
|
|
|
List<PlayoutHistory> playoutHistory = await dbContext.PlayoutHistory
|
|
.AsNoTracking()
|
|
.Where(h => h.PlayoutId == playoutId)
|
|
.ToListAsync();
|
|
|
|
return new PlayoutReferenceData(
|
|
channel,
|
|
Option<Deco>.None,
|
|
existingItems,
|
|
playoutTemplates,
|
|
programSchedule,
|
|
programScheduleAlternates,
|
|
playoutHistory,
|
|
TimeSpan.Zero);
|
|
}
|
|
}
|