Browse Source

playout rework to maintain collection progress (#720)

* initial work on maintaining playout state

* debugging wip

* fix refresh playout logic

* fix failing test

* more fixes

* update changelog

* comment out some debug logs

* comment out more logs
pull/713/head
Jason Dove 3 years ago committed by GitHub
parent
commit
c2eec2fc2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      CHANGELOG.md
  2. 12
      ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuildHandler.cs
  3. 13
      ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs
  4. 11
      ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs
  5. 12
      ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs
  6. 11
      ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs
  7. 11
      ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs
  8. 11
      ErsatzTV.Application/MediaCollections/Commands/AddOtherVideoToCollectionHandler.cs
  9. 11
      ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs
  10. 11
      ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs
  11. 7
      ErsatzTV.Application/MediaCollections/Commands/AddSongToCollectionHandler.cs
  12. 12
      ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs
  13. 12
      ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs
  14. 11
      ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs
  15. 59
      ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs
  16. 11
      ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs
  17. 3
      ErsatzTV.Application/Playouts/Commands/BuildPlayout.cs
  18. 17
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  19. 8
      ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs
  20. 9
      ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItemHandler.cs
  21. 9
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs
  22. 12
      ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs
  23. 27
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  24. 3139
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  25. 3
      ErsatzTV.Core/Domain/PlayoutItem.cs
  26. 16
      ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs
  27. 1
      ErsatzTV.Core/ErsatzTV.Core.csproj
  28. 24
      ErsatzTV.Core/FFmpeg/FFmpegSegmenterService.cs
  29. 1
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegSegmenterService.cs
  30. 2
      ErsatzTV.Core/Interfaces/FFmpeg/IHlsSessionWorker.cs
  31. 11
      ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs
  32. 13
      ErsatzTV.Core/Scheduling/PlayoutBuildMode.cs
  33. 328
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  34. 8
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs
  35. 8
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs
  36. 8
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs
  37. 12
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs
  38. 8
      ErsatzTV.Core/Scheduling/PlayoutParameters.cs
  39. 3
      ErsatzTV.Infrastructure/Data/Configurations/PlayoutProgramScheduleAnchorConfiguration.cs
  40. 3864
      ErsatzTV.Infrastructure/Migrations/20220209084802_Add_PlayoutProgramScheduleAnchor_AnchorDate.Designer.cs
  41. 26
      ErsatzTV.Infrastructure/Migrations/20220209084802_Add_PlayoutProgramScheduleAnchor_AnchorDate.cs
  42. 3
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  43. 36
      ErsatzTV/Pages/Playouts.razor
  44. 2
      ErsatzTV/Program.cs
  45. 20
      ErsatzTV/Services/SchedulerService.cs
  46. 20
      ErsatzTV/Shared/SchedulePlayoutReset.razor

9
CHANGELOG.md

@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Fixed
- Fix `HLS Segmenter` bug where it would drift off of the schedule if a playout was changed while the segmenter was running
### Added
- Add `Preferred Subtitle Language` and `Subtitle Mode` to channel settings
@ -15,6 +17,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -15,6 +17,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed
- Remove legacy transcoder logic option; all channels will use the new transcoder logic
- Renamed channel setting `Preferred Language` to `Preferred Audio Language`
- Reworked playout build logic to maintain collection progress in some scenarios. There are now three build modes:
- `Continue` - add new items to the end of an existing playout
- This mode is used when playouts are automatically extended in the background
- `Refresh` - this mode will try to maintain collection progress while rebuilding the entire playout
- This mode is used when a schedule is updated, or when collection modifications trigger a playout rebuild
- `Reset` - this mode will rebuild the entire playout and will NOT maintain progress
- This mode is only used when the `Reset Playout` button is clicked on the Playouts page
## [0.4.5-alpha] - 2022-03-29
### Fixed

12
ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuildHandler.cs

@ -3,13 +3,13 @@ using ErsatzTV.Application.Playouts; @@ -3,13 +3,13 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Configuration;
public class
UpdatePlayoutDaysToBuildHandler : MediatR.IRequestHandler<UpdatePlayoutDaysToBuild, Either<BaseError, Unit>>
public class UpdatePlayoutDaysToBuildHandler : IRequestHandler<UpdatePlayoutDaysToBuild, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
@ -37,16 +37,16 @@ public class @@ -37,16 +37,16 @@ public class
private async Task<Unit> ApplyUpdate(TvContext dbContext, int daysToBuild)
{
await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, daysToBuild);
// build all playouts to proper number of days
// continue all playouts to proper number of days
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Channel)
.ToListAsync();
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id))
{
await _workerChannel.WriteAsync(new BuildPlayout(playoutId));
await _workerChannel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Continue));
}
return Unit.Default;
}

13
ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs

@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -10,7 +11,7 @@ using Microsoft.EntityFrameworkCore; @@ -10,7 +11,7 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections;
public class AddArtistToCollectionHandler :
MediatR.IRequestHandler<AddArtistToCollection, Either<BaseError, Unit>>
IRequestHandler<AddArtistToCollection, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
@ -30,9 +31,9 @@ public class AddArtistToCollectionHandler : @@ -30,9 +31,9 @@ public class AddArtistToCollectionHandler :
AddArtistToCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, parameters => ApplyAddArtistRequest(dbContext, parameters));
return await validation.Apply(parameters => ApplyAddArtistRequest(dbContext, parameters));
}
private async Task<Unit> ApplyAddArtistRequest(TvContext dbContext, Parameters parameters)
@ -40,11 +41,11 @@ public class AddArtistToCollectionHandler : @@ -40,11 +41,11 @@ public class AddArtistToCollectionHandler :
parameters.Collection.MediaItems.Add(parameters.Artist);
if (await dbContext.SaveChangesAsync() > 0)
{
// rebuild all playouts that use this collection
// refresh all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(parameters.Collection.Id))
.PlayoutIdsUsingCollection(parameters.Collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}

11
ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs

@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -30,9 +31,9 @@ public class AddEpisodeToCollectionHandler : @@ -30,9 +31,9 @@ public class AddEpisodeToCollectionHandler :
AddEpisodeToCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, parameters => ApplyAddTelevisionEpisodeRequest(dbContext, parameters));
return await validation.Apply(parameters => ApplyAddTelevisionEpisodeRequest(dbContext, parameters));
}
private async Task<Unit> ApplyAddTelevisionEpisodeRequest(TvContext dbContext, Parameters parameters)
@ -40,11 +41,11 @@ public class AddEpisodeToCollectionHandler : @@ -40,11 +41,11 @@ public class AddEpisodeToCollectionHandler :
parameters.Collection.MediaItems.Add(parameters.Episode);
if (await dbContext.SaveChangesAsync() > 0)
{
// rebuild all playouts that use this collection
// refresh all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(parameters.Collection.Id))
.PlayoutIdsUsingCollection(parameters.Collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}

12
ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs

@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -36,10 +37,9 @@ public class AddItemsToCollectionHandler : @@ -36,10 +37,9 @@ public class AddItemsToCollectionHandler :
AddItemsToCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => ApplyAddItemsRequest(dbContext, c, request));
return await validation.Apply(c => ApplyAddItemsRequest(dbContext, c, request));
}
private async Task<Unit> ApplyAddItemsRequest(TvContext dbContext, Collection collection, AddItemsToCollection request)
@ -63,11 +63,11 @@ public class AddItemsToCollectionHandler : @@ -63,11 +63,11 @@ public class AddItemsToCollectionHandler :
if (await dbContext.SaveChangesAsync() > 0)
{
// rebuild all playouts that use this collection
// refresh all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId))
.PlayoutIdsUsingCollection(request.CollectionId))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}

11
ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs

@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -30,9 +31,9 @@ public class AddMovieToCollectionHandler : @@ -30,9 +31,9 @@ public class AddMovieToCollectionHandler :
AddMovieToCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, parameters => ApplyAddMovieRequest(dbContext, parameters));
return await validation.Apply(parameters => ApplyAddMovieRequest(dbContext, parameters));
}
private async Task<Unit> ApplyAddMovieRequest(TvContext dbContext, Parameters parameters)
@ -40,11 +41,11 @@ public class AddMovieToCollectionHandler : @@ -40,11 +41,11 @@ public class AddMovieToCollectionHandler :
parameters.Collection.MediaItems.Add(parameters.Movie);
if (await dbContext.SaveChangesAsync() > 0)
{
// rebuild all playouts that use this collection
// refresh all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(parameters.Collection.Id))
.PlayoutIdsUsingCollection(parameters.Collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}

11
ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs

@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -30,9 +31,9 @@ public class AddMusicVideoToCollectionHandler : @@ -30,9 +31,9 @@ public class AddMusicVideoToCollectionHandler :
AddMusicVideoToCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, parameters => ApplyAddMusicVideoRequest(dbContext, parameters));
return await validation.Apply(parameters => ApplyAddMusicVideoRequest(dbContext, parameters));
}
private async Task<Unit> ApplyAddMusicVideoRequest(TvContext dbContext, Parameters parameters)
@ -40,11 +41,11 @@ public class AddMusicVideoToCollectionHandler : @@ -40,11 +41,11 @@ public class AddMusicVideoToCollectionHandler :
parameters.Collection.MediaItems.Add(parameters.MusicVideo);
if (await dbContext.SaveChangesAsync() > 0)
{
// rebuild all playouts that use this collection
// refresh all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(parameters.Collection.Id))
.PlayoutIdsUsingCollection(parameters.Collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}

11
ErsatzTV.Application/MediaCollections/Commands/AddOtherVideoToCollectionHandler.cs

@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -30,9 +31,9 @@ public class AddOtherVideoToCollectionHandler : @@ -30,9 +31,9 @@ public class AddOtherVideoToCollectionHandler :
AddOtherVideoToCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, parameters => ApplyAddOtherVideoRequest(dbContext, parameters));
return await validation.Apply(parameters => ApplyAddOtherVideoRequest(dbContext, parameters));
}
private async Task<Unit> ApplyAddOtherVideoRequest(TvContext dbContext, Parameters parameters)
@ -40,11 +41,11 @@ public class AddOtherVideoToCollectionHandler : @@ -40,11 +41,11 @@ public class AddOtherVideoToCollectionHandler :
parameters.Collection.MediaItems.Add(parameters.OtherVideo);
if (await dbContext.SaveChangesAsync() > 0)
{
// rebuild all playouts that use this collection
// refresh all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(parameters.Collection.Id))
.PlayoutIdsUsingCollection(parameters.Collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}

11
ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs

@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -30,9 +31,9 @@ public class AddSeasonToCollectionHandler : @@ -30,9 +31,9 @@ public class AddSeasonToCollectionHandler :
AddSeasonToCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, parameters => ApplyAddSeasonRequest(dbContext, parameters));
return await validation.Apply(parameters => ApplyAddSeasonRequest(dbContext, parameters));
}
private async Task<Unit> ApplyAddSeasonRequest(TvContext dbContext, Parameters parameters)
@ -40,11 +41,11 @@ public class AddSeasonToCollectionHandler : @@ -40,11 +41,11 @@ public class AddSeasonToCollectionHandler :
parameters.Collection.MediaItems.Add(parameters.Season);
if (await dbContext.SaveChangesAsync() > 0)
{
// rebuild all playouts that use this collection
// refresh all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(parameters.Collection.Id))
.PlayoutIdsUsingCollection(parameters.Collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}

11
ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs

@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -30,9 +31,9 @@ public class AddShowToCollectionHandler : @@ -30,9 +31,9 @@ public class AddShowToCollectionHandler :
AddShowToCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, parameters => ApplyAddShowRequest(dbContext, parameters));
return await validation.Apply(parameters => ApplyAddShowRequest(dbContext, parameters));
}
private async Task<Unit> ApplyAddShowRequest(TvContext dbContext, Parameters parameters)
@ -40,11 +41,11 @@ public class AddShowToCollectionHandler : @@ -40,11 +41,11 @@ public class AddShowToCollectionHandler :
parameters.Collection.MediaItems.Add(parameters.Show);
if (await dbContext.SaveChangesAsync() > 0)
{
// rebuild all playouts that use this collection
// refresh all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(parameters.Collection.Id))
.PlayoutIdsUsingCollection(parameters.Collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}

7
ErsatzTV.Application/MediaCollections/Commands/AddSongToCollectionHandler.cs

@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -40,11 +41,11 @@ public class AddSongToCollectionHandler : @@ -40,11 +41,11 @@ public class AddSongToCollectionHandler :
parameters.Collection.MediaItems.Add(parameters.Song);
if (await dbContext.SaveChangesAsync() > 0)
{
// rebuild all playouts that use this collection
// refresh all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(parameters.Collection.Id))
.PlayoutIdsUsingCollection(parameters.Collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}

12
ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs

@ -3,14 +3,14 @@ using ErsatzTV.Application.Playouts; @@ -3,14 +3,14 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections;
public class RemoveItemsFromCollectionHandler :
MediatR.IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>>
public class RemoveItemsFromCollectionHandler : IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
@ -30,9 +30,9 @@ public class RemoveItemsFromCollectionHandler : @@ -30,9 +30,9 @@ public class RemoveItemsFromCollectionHandler :
RemoveItemsFromCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => ApplyRemoveItemsRequest(dbContext, request, c));
return await validation.Apply(c => ApplyRemoveItemsRequest(dbContext, request, c));
}
private async Task<Unit> ApplyRemoveItemsRequest(
@ -48,10 +48,10 @@ public class RemoveItemsFromCollectionHandler : @@ -48,10 +48,10 @@ public class RemoveItemsFromCollectionHandler :
if (itemsToRemove.Any() && await dbContext.SaveChangesAsync() > 0)
{
// rebuild all playouts that use this collection
// refresh all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}

12
ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs

@ -3,14 +3,14 @@ using ErsatzTV.Application.Playouts; @@ -3,14 +3,14 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections;
public class UpdateCollectionCustomOrderHandler :
MediatR.IRequestHandler<UpdateCollectionCustomOrder, Either<BaseError, Unit>>
public class UpdateCollectionCustomOrderHandler : IRequestHandler<UpdateCollectionCustomOrder, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
@ -30,9 +30,9 @@ public class UpdateCollectionCustomOrderHandler : @@ -30,9 +30,9 @@ public class UpdateCollectionCustomOrderHandler :
UpdateCollectionCustomOrder request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request));
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
}
private async Task<Unit> ApplyUpdateRequest(
@ -53,11 +53,11 @@ public class UpdateCollectionCustomOrderHandler : @@ -53,11 +53,11 @@ public class UpdateCollectionCustomOrderHandler :
if (await dbContext.SaveChangesAsync() > 0)
{
// rebuild all playouts that use this collection
// refresh all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}

11
ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs

@ -3,13 +3,14 @@ using ErsatzTV.Application.Playouts; @@ -3,13 +3,14 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections;
public class UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection, Either<BaseError, Unit>>
public class UpdateCollectionHandler : IRequestHandler<UpdateCollection, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
@ -29,9 +30,9 @@ public class UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection, @@ -29,9 +30,9 @@ public class UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection,
UpdateCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request));
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
}
private async Task<Unit> ApplyUpdateRequest(TvContext dbContext, Collection c, UpdateCollection request)
@ -44,11 +45,11 @@ public class UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection, @@ -44,11 +45,11 @@ public class UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection,
if (await dbContext.SaveChangesAsync() > 0 && request.UseCustomPlaybackOrder.IsSome)
{
// rebuild all playouts that use this collection
// refresh all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(
request.CollectionId))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}

59
ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs

@ -3,13 +3,14 @@ using ErsatzTV.Application.Playouts; @@ -3,13 +3,14 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections;
public class UpdateMultiCollectionHandler : MediatR.IRequestHandler<UpdateMultiCollection, Either<BaseError, Unit>>
public class UpdateMultiCollectionHandler : IRequestHandler<UpdateMultiCollection, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
@ -29,15 +30,15 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler<UpdateMultiC @@ -29,15 +30,15 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler<UpdateMultiC
UpdateMultiCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, MultiCollection> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request));
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
}
private async Task<Unit> ApplyUpdateRequest(TvContext dbContext, MultiCollection c, UpdateMultiCollection request)
{
c.Name = request.Name;
// save name first so playouts don't get rebuilt for a name change
await dbContext.SaveChangesAsync();
@ -45,22 +46,23 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler<UpdateMultiC @@ -45,22 +46,23 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler<UpdateMultiC
.Filter(i => i.CollectionId.HasValue)
// ReSharper disable once PossibleInvalidOperationException
.Filter(i => c.MultiCollectionItems.All(i2 => i2.CollectionId != i.CollectionId.Value))
.Map(i => new MultiCollectionItem
{
// ReSharper disable once PossibleInvalidOperationException
CollectionId = i.CollectionId.Value,
MultiCollectionId = c.Id,
ScheduleAsGroup = i.ScheduleAsGroup,
PlaybackOrder = i.PlaybackOrder
})
.Map(
i => new MultiCollectionItem
{
// ReSharper disable once PossibleInvalidOperationException
CollectionId = i.CollectionId.Value,
MultiCollectionId = c.Id,
ScheduleAsGroup = i.ScheduleAsGroup,
PlaybackOrder = i.PlaybackOrder
})
.ToList();
var toRemove = c.MultiCollectionItems
.Filter(i => request.Items.All(i2 => i2.CollectionId != i.CollectionId))
.ToList();
// remove items that are no longer present
c.MultiCollectionItems.RemoveAll(toRemove.Contains);
// update existing items
foreach (MultiCollectionItem item in c.MultiCollectionItems)
{
@ -79,22 +81,23 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler<UpdateMultiC @@ -79,22 +81,23 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler<UpdateMultiC
.Filter(i => i.SmartCollectionId.HasValue)
// ReSharper disable once PossibleInvalidOperationException
.Filter(i => c.MultiCollectionSmartItems.All(i2 => i2.SmartCollectionId != i.SmartCollectionId.Value))
.Map(i => new MultiCollectionSmartItem
{
// ReSharper disable once PossibleInvalidOperationException
SmartCollectionId = i.SmartCollectionId.Value,
MultiCollectionId = c.Id,
ScheduleAsGroup = i.ScheduleAsGroup,
PlaybackOrder = i.PlaybackOrder
})
.Map(
i => new MultiCollectionSmartItem
{
// ReSharper disable once PossibleInvalidOperationException
SmartCollectionId = i.SmartCollectionId.Value,
MultiCollectionId = c.Id,
ScheduleAsGroup = i.ScheduleAsGroup,
PlaybackOrder = i.PlaybackOrder
})
.ToList();
var toRemoveSmart = c.MultiCollectionSmartItems
.Filter(i => request.Items.All(i2 => i2.SmartCollectionId != i.SmartCollectionId))
.ToList();
// remove items that are no longer present
c.MultiCollectionSmartItems.RemoveAll(toRemoveSmart.Contains);
// update existing items
foreach (MultiCollectionSmartItem item in c.MultiCollectionSmartItems)
{
@ -112,11 +115,11 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler<UpdateMultiC @@ -112,11 +115,11 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler<UpdateMultiC
// rebuild playouts
if (await dbContext.SaveChangesAsync() > 0)
{
// rebuild all playouts that use this collection
// refresh all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingMultiCollection(
request.MultiCollectionId))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}
@ -138,7 +141,9 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler<UpdateMultiC @@ -138,7 +141,9 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler<UpdateMultiC
.SelectOneAsync(c => c.Id, c => c.Id == updateCollection.MultiCollectionId)
.Map(o => o.ToValidation<BaseError>("MultiCollection does not exist."));
private static async Task<Validation<BaseError, string>> ValidateName(TvContext dbContext, UpdateMultiCollection updateMultiCollection)
private static async Task<Validation<BaseError, string>> ValidateName(
TvContext dbContext,
UpdateMultiCollection updateMultiCollection)
{
List<string> allNames = await dbContext.MultiCollections
.Filter(mc => mc.Id != updateMultiCollection.MultiCollectionId)

11
ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs

@ -3,13 +3,14 @@ using ErsatzTV.Application.Playouts; @@ -3,13 +3,14 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections;
public class UpdateSmartCollectionHandler : MediatR.IRequestHandler<UpdateSmartCollection, Either<BaseError, Unit>>
public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollection, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
@ -29,9 +30,9 @@ public class UpdateSmartCollectionHandler : MediatR.IRequestHandler<UpdateSmartC @@ -29,9 +30,9 @@ public class UpdateSmartCollectionHandler : MediatR.IRequestHandler<UpdateSmartC
UpdateSmartCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, SmartCollection> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request));
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
}
private async Task<Unit> ApplyUpdateRequest(TvContext dbContext, SmartCollection c, UpdateSmartCollection request)
@ -41,10 +42,10 @@ public class UpdateSmartCollectionHandler : MediatR.IRequestHandler<UpdateSmartC @@ -41,10 +42,10 @@ public class UpdateSmartCollectionHandler : MediatR.IRequestHandler<UpdateSmartC
// rebuild playouts
if (await dbContext.SaveChangesAsync() > 0)
{
// rebuild all playouts that use this smart collection
// refresh all playouts that use this smart collection
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingSmartCollection(request.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}

3
ErsatzTV.Application/Playouts/Commands/BuildPlayout.cs

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Application.Playouts;
public record BuildPlayout(int PlayoutId, bool Rebuild = false) : MediatR.IRequest<Either<BaseError, Unit>>,
public record BuildPlayout(int PlayoutId, PlayoutBuildMode Mode) : IRequest<Either<BaseError, Unit>>,
IBackgroundServiceRequest;

17
ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
@ -13,12 +14,18 @@ public class BuildPlayoutHandler : MediatR.IRequestHandler<BuildPlayout, Either< @@ -13,12 +14,18 @@ public class BuildPlayoutHandler : MediatR.IRequestHandler<BuildPlayout, Either<
private readonly IClient _client;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IPlayoutBuilder _playoutBuilder;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
public BuildPlayoutHandler(IClient client, IDbContextFactory<TvContext> dbContextFactory, IPlayoutBuilder playoutBuilder)
public BuildPlayoutHandler(
IClient client,
IDbContextFactory<TvContext> dbContextFactory,
IPlayoutBuilder playoutBuilder,
IFFmpegSegmenterService ffmpegSegmenterService)
{
_client = client;
_dbContextFactory = dbContextFactory;
_playoutBuilder = playoutBuilder;
_ffmpegSegmenterService = ffmpegSegmenterService;
}
public async Task<Either<BaseError, Unit>> Handle(BuildPlayout request, CancellationToken cancellationToken)
@ -32,8 +39,11 @@ public class BuildPlayoutHandler : MediatR.IRequestHandler<BuildPlayout, Either< @@ -32,8 +39,11 @@ public class BuildPlayoutHandler : MediatR.IRequestHandler<BuildPlayout, Either<
{
try
{
await _playoutBuilder.BuildPlayoutItems(playout, request.Rebuild);
await dbContext.SaveChangesAsync();
await _playoutBuilder.Build(playout, request.Mode);
if (await dbContext.SaveChangesAsync() > 0)
{
_ffmpegSegmenterService.PlayoutUpdated(playout.Channel.Number);
}
}
catch (Exception ex)
{
@ -42,7 +52,6 @@ public class BuildPlayoutHandler : MediatR.IRequestHandler<BuildPlayout, Either< @@ -42,7 +52,6 @@ public class BuildPlayoutHandler : MediatR.IRequestHandler<BuildPlayout, Either<
return Unit.Default;
}
private static Task<Validation<BaseError, Playout>> Validate(TvContext dbContext, BuildPlayout request) =>
PlayoutMustExist(dbContext, request);

8
ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
using System.Threading.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -25,17 +26,16 @@ public class CreatePlayoutHandler : IRequestHandler<CreatePlayout, Either<BaseEr @@ -25,17 +26,16 @@ public class CreatePlayoutHandler : IRequestHandler<CreatePlayout, Either<BaseEr
CreatePlayout request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Playout> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, playout => PersistPlayout(dbContext, playout));
return await validation.Apply(playout => PersistPlayout(dbContext, playout));
}
private async Task<CreatePlayoutResponse> PersistPlayout(TvContext dbContext, Playout playout)
{
await dbContext.Playouts.AddAsync(playout);
await dbContext.SaveChangesAsync();
await _channel.WriteAsync(new BuildPlayout(playout.Id));
await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset));
return new CreatePlayoutResponse(playout.Id);
}

9
ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItemHandler.cs

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.ProgramSchedules.Mapper;
@ -26,9 +27,9 @@ public class AddProgramScheduleItemHandler : ProgramScheduleItemCommandBase, @@ -26,9 +27,9 @@ public class AddProgramScheduleItemHandler : ProgramScheduleItemCommandBase,
AddProgramScheduleItem request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, ps => PersistItem(dbContext, request, ps));
return await validation.Apply(ps => PersistItem(dbContext, request, ps));
}
private async Task<ProgramScheduleItemViewModel> PersistItem(
@ -43,10 +44,10 @@ public class AddProgramScheduleItemHandler : ProgramScheduleItemCommandBase, @@ -43,10 +44,10 @@ public class AddProgramScheduleItemHandler : ProgramScheduleItemCommandBase,
await dbContext.SaveChangesAsync();
// rebuild any playouts that use this schedule
// refresh any playouts that use this schedule
foreach (Playout playout in programSchedule.Playouts)
{
await _channel.WriteAsync(new BuildPlayout(playout.Id, true));
await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Refresh));
}
return ProjectToViewModel(item);

9
ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.ProgramSchedules.Mapper;
@ -26,9 +27,9 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase @@ -26,9 +27,9 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase
ReplaceProgramScheduleItems request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, ps => PersistItems(dbContext, request, ps));
return await validation.Apply(ps => PersistItems(dbContext, request, ps));
}
private async Task<IEnumerable<ProgramScheduleItemViewModel>> PersistItems(
@ -41,10 +42,10 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase @@ -41,10 +42,10 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase
await dbContext.SaveChangesAsync();
// rebuild any playouts that use this schedule
// refresh any playouts that use this schedule
foreach (Playout playout in programSchedule.Playouts)
{
await _channel.WriteAsync(new BuildPlayout(playout.Id, true));
await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Refresh));
}
return programSchedule.Items.Map(ProjectToViewModel);

12
ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -27,9 +28,8 @@ public class UpdateProgramScheduleHandler : @@ -27,9 +28,8 @@ public class UpdateProgramScheduleHandler :
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, ps => ApplyUpdateRequest(dbContext, ps, request));
return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request));
}
private async Task<UpdateProgramScheduleResult> ApplyUpdateRequest(
@ -37,8 +37,8 @@ public class UpdateProgramScheduleHandler : @@ -37,8 +37,8 @@ public class UpdateProgramScheduleHandler :
ProgramSchedule programSchedule,
UpdateProgramSchedule request)
{
// we need to rebuild playouts if the playback order or keep multi-episodes has been modified
bool needToRebuildPlayout =
// we need to refresh playouts if the playback order or keep multi-episodes has been modified
bool needToRefreshPlayout =
programSchedule.KeepMultiPartEpisodesTogether != request.KeepMultiPartEpisodesTogether ||
programSchedule.TreatCollectionsAsShows != request.TreatCollectionsAsShows ||
programSchedule.ShuffleScheduleItems != request.ShuffleScheduleItems;
@ -51,7 +51,7 @@ public class UpdateProgramScheduleHandler : @@ -51,7 +51,7 @@ public class UpdateProgramScheduleHandler :
await dbContext.SaveChangesAsync();
if (needToRebuildPlayout)
if (needToRefreshPlayout)
{
List<int> playoutIds = await dbContext.Playouts
.Filter(p => p.ProgramScheduleId == programSchedule.Id)
@ -60,7 +60,7 @@ public class UpdateProgramScheduleHandler : @@ -60,7 +60,7 @@ public class UpdateProgramScheduleHandler :
foreach (int playoutId in playoutIds)
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
}
}

27
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -30,6 +30,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -30,6 +30,7 @@ public class HlsSessionWorker : IHlsSessionWorker
private DateTimeOffset _playlistStart;
private Option<int> _targetFramerate;
private string _channelNumber;
private bool _firstProcess;
public HlsSessionWorker(
IHlsPlaylistFilter hlsPlaylistFilter,
@ -70,6 +71,8 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -70,6 +71,8 @@ public class HlsSessionWorker : IHlsSessionWorker
}
}
public void PlayoutUpdated() => _firstProcess = true;
public async Task Run(string channelNumber, TimeSpan idleTimeout, CancellationToken incomingCancellationToken)
{
var cts = CancellationTokenSource.CreateLinkedTokenSource(incomingCancellationToken);
@ -105,8 +108,10 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -105,8 +108,10 @@ public class HlsSessionWorker : IHlsSessionWorker
_transcodedUntil = DateTimeOffset.Now;
_playlistStart = _transcodedUntil;
_firstProcess = true;
bool initialWorkAhead = Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit();
if (!await Transcode(true, !initialWorkAhead, cancellationToken))
if (!await Transcode(!initialWorkAhead, cancellationToken))
{
return;
}
@ -127,7 +132,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -127,7 +132,7 @@ public class HlsSessionWorker : IHlsSessionWorker
bool realtime = transcodedBuffer >= TimeSpan.FromSeconds(30);
bool subsequentWorkAhead =
!realtime && Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit();
if (!await Transcode(false, !subsequentWorkAhead, cancellationToken))
if (!await Transcode(!subsequentWorkAhead, cancellationToken))
{
return;
}
@ -149,7 +154,6 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -149,7 +154,6 @@ public class HlsSessionWorker : IHlsSessionWorker
}
private async Task<bool> Transcode(
bool firstProcess,
bool realtime,
CancellationToken cancellationToken)
{
@ -177,8 +181,8 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -177,8 +181,8 @@ public class HlsSessionWorker : IHlsSessionWorker
var request = new GetPlayoutItemProcessByChannelNumber(
_channelNumber,
"segmenter",
firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1),
!firstProcess,
_firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1),
!_firstProcess,
realtime,
ptsOffset,
_targetFramerate);
@ -220,6 +224,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -220,6 +224,7 @@ public class HlsSessionWorker : IHlsSessionWorker
{
_logger.LogInformation("HLS process has completed for channel {Channel}", _channelNumber);
_transcodedUntil = processModel.Until;
_firstProcess = false;
return true;
}
else
@ -237,7 +242,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -237,7 +242,7 @@ public class HlsSessionWorker : IHlsSessionWorker
_channelNumber,
commandResult.ExitCode,
commandResult.StandardError);
Either<BaseError, PlayoutItemProcessModel> maybeOfflineProcess = await mediator.Send(
new GetErrorProcess(
_channelNumber,
@ -252,7 +257,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -252,7 +257,7 @@ public class HlsSessionWorker : IHlsSessionWorker
foreach (PlayoutItemProcessModel errorProcessModel in maybeOfflineProcess.RightAsEnumerable())
{
Process errorProcess = errorProcessModel.Process;
_logger.LogInformation(
"ffmpeg hls error arguments {FFmpegArguments}",
string.Join(" ", errorProcess.StartInfo.ArgumentList));
@ -261,8 +266,12 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -261,8 +266,12 @@ public class HlsSessionWorker : IHlsSessionWorker
.WithArguments(errorProcess.StartInfo.ArgumentList)
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(cancellationToken);
return commandResult.ExitCode == 0;
if (commandResult.ExitCode == 0)
{
_firstProcess = false;
return true;
}
}
return false;

3139
ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs

File diff suppressed because it is too large Load Diff

3
ErsatzTV.Core/Domain/PlayoutItem.cs

@ -3,7 +3,7 @@ using ErsatzTV.Core.Domain.Filler; @@ -3,7 +3,7 @@ using ErsatzTV.Core.Domain.Filler;
namespace ErsatzTV.Core.Domain;
[DebuggerDisplay("{MediaItemId} - {Start} - {Finish}")]
[DebuggerDisplay("{MediaItemId} - {StartOffset} - {FinishOffset}")]
public class PlayoutItem
{
public int Id { get; set; }
@ -23,6 +23,7 @@ public class PlayoutItem @@ -23,6 +23,7 @@ public class PlayoutItem
public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime();
public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime();
public DateTimeOffset? GuideFinishOffset => GuideFinish.HasValue
? new DateTimeOffset(GuideFinish.Value, TimeSpan.Zero).ToLocalTime()
: null;

16
ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs

@ -1,12 +1,26 @@ @@ -1,12 +1,26 @@
namespace ErsatzTV.Core.Domain;
using Destructurama.Attributed;
namespace ErsatzTV.Core.Domain;
public class PlayoutProgramScheduleAnchor
{
public int Id { get; set; }
public int PlayoutId { get; set; }
[NotLogged]
public Playout Playout { get; set; }
public int ProgramScheduleId { get; set; }
[NotLogged]
public ProgramSchedule ProgramSchedule { get; set; }
public DateTime? AnchorDate { get; set; }
public DateTimeOffset? AnchorDateOffset => AnchorDate.HasValue
? new DateTimeOffset(AnchorDate.Value, TimeSpan.Zero).ToLocalTime()
: null;
public ProgramScheduleItemCollectionType CollectionType { get; set; }
public int? CollectionId { get; set; }
public Collection Collection { get; set; }

1
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -8,6 +8,7 @@ @@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Bugsnag" Version="3.0.0" />
<PackageReference Include="Destructurama.Attributed" Version="3.0.0" />
<PackageReference Include="Flurl" Version="3.0.4" />
<PackageReference Include="LanguageExt.Core" Version="4.0.4" />
<PackageReference Include="MediatR" Version="10.0.1" />

24
ErsatzTV.Core/FFmpeg/FFmpegSegmenterService.cs

@ -1,12 +1,17 @@ @@ -1,12 +1,17 @@
using System.Collections.Concurrent;
using ErsatzTV.Core.Interfaces.FFmpeg;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.FFmpeg;
public class FFmpegSegmenterService : IFFmpegSegmenterService
{
public FFmpegSegmenterService()
private readonly ILogger<FFmpegSegmenterService> _logger;
public FFmpegSegmenterService(ILogger<FFmpegSegmenterService> logger)
{
_logger = logger;
SessionWorkers = new ConcurrentDictionary<string, IHlsSessionWorker>();
}
@ -19,4 +24,19 @@ public class FFmpegSegmenterService : IFFmpegSegmenterService @@ -19,4 +24,19 @@ public class FFmpegSegmenterService : IFFmpegSegmenterService
worker?.Touch();
}
}
}
public void PlayoutUpdated(string channelNumber)
{
if (SessionWorkers.TryGetValue(channelNumber, out IHlsSessionWorker worker))
{
if (worker != null)
{
_logger.LogInformation(
"Playout has been updated for channel {ChannelNumber}, HLS segmenter will skip ahead to catch up",
channelNumber);
worker.PlayoutUpdated();
}
}
}
}

1
ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegSegmenterService.cs

@ -7,4 +7,5 @@ public interface IFFmpegSegmenterService @@ -7,4 +7,5 @@ public interface IFFmpegSegmenterService
ConcurrentDictionary<string, IHlsSessionWorker> SessionWorkers { get; }
void TouchChannel(string channelNumber);
void PlayoutUpdated(string channelNumber);
}

2
ErsatzTV.Core/Interfaces/FFmpeg/IHlsSessionWorker.cs

@ -4,7 +4,7 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg; @@ -4,7 +4,7 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg;
public interface IHlsSessionWorker
{
DateTimeOffset PlaylistStart { get; }
void Touch();
Task<Option<TrimPlaylistResult>> TrimPlaylist(DateTimeOffset filterBefore, CancellationToken cancellationToken);
void PlayoutUpdated();
}

11
ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs

@ -1,14 +1,9 @@ @@ -1,14 +1,9 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IPlayoutBuilder
{
Task<Playout> BuildPlayoutItems(Playout playout, bool rebuild = false);
Task<Playout> BuildPlayoutItems(
Playout playout,
DateTimeOffset playoutStart,
DateTimeOffset playoutFinish,
bool rebuild = false);
Task<Playout> Build(Playout playout, PlayoutBuildMode mode);
}

13
ErsatzTV.Core/Scheduling/PlayoutBuildMode.cs

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
namespace ErsatzTV.Core.Scheduling;
public enum PlayoutBuildMode
{
// this continues building playout into the future
Continue = 1,
// this rebuilds a playout but will maintain collection progress
Refresh = 2,
// this rebuilds a playout and clears all state
Reset = 3
}

328
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -32,18 +32,190 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -32,18 +32,190 @@ public class PlayoutBuilder : IPlayoutBuilder
_logger = logger;
}
public async Task<Playout> BuildPlayoutItems(Playout playout, bool rebuild = false)
public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode)
{
DateTimeOffset now = DateTimeOffset.Now;
Option<int> daysToBuild = await _configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild);
return await BuildPlayoutItems(playout, now, now.AddDays(await daysToBuild.IfNoneAsync(2)), rebuild);
foreach (PlayoutParameters parameters in await Validate(playout))
{
// for testing purposes
// if (mode == PlayoutBuildMode.Reset)
// {
// return await Build(playout, mode, parameters with { Start = parameters.Start.AddDays(-2) });
// }
return await Build(playout, mode, parameters);
}
return playout;
}
public async Task<Playout> BuildPlayoutItems(
private Task<Playout> Build(Playout playout, PlayoutBuildMode mode, PlayoutParameters parameters) =>
mode switch
{
PlayoutBuildMode.Refresh => RefreshPlayout(playout, parameters),
PlayoutBuildMode.Reset => ResetPlayout(playout, parameters),
_ => ContinuePlayout(playout, parameters)
};
internal async Task<Playout> Build(
Playout playout,
DateTimeOffset playoutStart,
DateTimeOffset playoutFinish,
bool rebuild = false)
PlayoutBuildMode mode,
DateTimeOffset start,
DateTimeOffset finish)
{
foreach (PlayoutParameters parameters in await Validate(playout))
{
return await Build(playout, mode, parameters with { Start = start, Finish = finish });
}
return playout;
}
private async Task<Playout> RefreshPlayout(Playout playout, PlayoutParameters parameters)
{
_logger.LogDebug(
"Refreshing playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
playout.Id,
playout.Channel.Number,
playout.Channel.Name);
playout.Items.Clear();
playout.Anchor = null;
// foreach (PlayoutProgramScheduleAnchor anchor in playout.ProgramScheduleAnchors)
// {
// anchor.Playout = null;
// anchor.Collection = null;
// anchor.ProgramSchedule = null;
// }
// _logger.LogDebug("All anchors: {@Anchors}", playout.ProgramScheduleAnchors);
// remove null anchor date ("continue" anchors)
playout.ProgramScheduleAnchors.RemoveAll(a => a.AnchorDate is null);
// _logger.LogDebug("Checkpoint anchors: {@Anchors}", playout.ProgramScheduleAnchors);
// remove old checkpoints
playout.ProgramScheduleAnchors.RemoveAll(
a => a.AnchorDateOffset.IfNone(SystemTime.MaxValueUtc) < parameters.Start.Date);
// remove new checkpoints
playout.ProgramScheduleAnchors.RemoveAll(
a => a.AnchorDateOffset.IfNone(SystemTime.MinValueUtc).Date > parameters.Start.Date);
// _logger.LogDebug("Remaining anchors: {@Anchors}", playout.ProgramScheduleAnchors);
var allAnchors = playout.ProgramScheduleAnchors.ToList();
var collectionIds = playout.ProgramScheduleAnchors.Map(a => Optional(a.CollectionId)).Somes().ToHashSet();
var multiCollectionIds =
playout.ProgramScheduleAnchors.Map(a => Optional(a.MultiCollectionId)).Somes().ToHashSet();
var smartCollectionIds =
playout.ProgramScheduleAnchors.Map(a => Optional(a.SmartCollectionId)).Somes().ToHashSet();
var mediaItemIds = playout.ProgramScheduleAnchors.Map(a => Optional(a.MediaItemId)).Somes().ToHashSet();
playout.ProgramScheduleAnchors.Clear();
foreach (int collectionId in collectionIds)
{
PlayoutProgramScheduleAnchor minAnchor = allAnchors.Filter(a => a.CollectionId == collectionId)
.MinBy(a => a.AnchorDateOffset.IfNone(DateTimeOffset.MaxValue).Ticks);
playout.ProgramScheduleAnchors.Add(minAnchor);
}
foreach (int multiCollectionId in multiCollectionIds)
{
PlayoutProgramScheduleAnchor minAnchor = allAnchors.Filter(a => a.MultiCollectionId == multiCollectionId)
.MinBy(a => a.AnchorDateOffset.IfNone(DateTimeOffset.MaxValue).Ticks);
playout.ProgramScheduleAnchors.Add(minAnchor);
}
foreach (int smartCollectionId in smartCollectionIds)
{
PlayoutProgramScheduleAnchor minAnchor = allAnchors.Filter(a => a.SmartCollectionId == smartCollectionId)
.MinBy(a => a.AnchorDateOffset.IfNone(DateTimeOffset.MaxValue).Ticks);
playout.ProgramScheduleAnchors.Add(minAnchor);
}
foreach (int mediaItemId in mediaItemIds)
{
PlayoutProgramScheduleAnchor minAnchor = allAnchors.Filter(a => a.MediaItemId == mediaItemId)
.MinBy(a => a.AnchorDateOffset.IfNone(DateTimeOffset.MaxValue).Ticks);
playout.ProgramScheduleAnchors.Add(minAnchor);
}
// _logger.LogDebug("Oldest anchors for each collection: {@Anchors}", playout.ProgramScheduleAnchors);
// convert checkpoints to non-checkpoints
// foreach (PlayoutProgramScheduleAnchor anchor in playout.ProgramScheduleAnchors)
// {
// anchor.AnchorDate = null;
// }
// _logger.LogDebug("Final anchors: {@Anchors}", playout.ProgramScheduleAnchors);
Option<DateTime> maybeAnchorDate = playout.ProgramScheduleAnchors
.Map(a => Optional(a.AnchorDate))
.Somes()
.HeadOrNone();
foreach (DateTime anchorDate in maybeAnchorDate)
{
playout.Anchor = new PlayoutAnchor
{
NextStart = anchorDate
};
}
return await BuildPlayoutItems(
playout,
parameters.Start,
parameters.Finish,
parameters.CollectionMediaItems);
}
private async Task<Playout> ResetPlayout(Playout playout, PlayoutParameters parameters)
{
_logger.LogDebug(
"Resetting playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
playout.Id,
playout.Channel.Number,
playout.Channel.Name);
playout.Items.Clear();
playout.Anchor = null;
playout.ProgramScheduleAnchors.Clear();
await BuildPlayoutItems(
playout,
parameters.Start,
parameters.Finish,
parameters.CollectionMediaItems);
return playout;
}
private async Task<Playout> ContinuePlayout(Playout playout, PlayoutParameters parameters)
{
_logger.LogDebug(
"Building playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
playout.Id,
playout.Channel.Number,
playout.Channel.Name);
await BuildPlayoutItems(
playout,
parameters.Start,
parameters.Finish,
parameters.CollectionMediaItems);
return playout;
}
private async Task<Option<PlayoutParameters>> Validate(Playout playout)
{
Map<CollectionKey, List<MediaItem>> collectionMediaItems = await GetCollectionMediaItems(playout);
if (!collectionMediaItems.Any())
@ -53,16 +225,9 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -53,16 +225,9 @@ public class PlayoutBuilder : IPlayoutBuilder
playout.Channel.Name,
playout.ProgramSchedule.Name);
return playout;
return None;
}
_logger.LogDebug(
"{Action} playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
rebuild ? "Rebuilding" : "Building",
playout.Id,
playout.Channel.Number,
playout.Channel.Name);
Option<CollectionKey> maybeEmptyCollection = await CheckForEmptyCollections(collectionMediaItems);
foreach (CollectionKey emptyCollection in maybeEmptyCollection)
{
@ -84,19 +249,76 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -84,19 +249,76 @@ public class PlayoutBuilder : IPlayoutBuilder
emptyCollection);
}
return playout;
return None;
}
playout.Items ??= new List<PlayoutItem>();
playout.ProgramScheduleAnchors ??= new List<PlayoutProgramScheduleAnchor>();
if (rebuild)
Option<int> daysToBuild = await _configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild);
DateTimeOffset now = DateTimeOffset.Now;
return new PlayoutParameters(
now,
now.AddDays(await daysToBuild.IfNoneAsync(2)),
collectionMediaItems);
}
private async Task<Playout> BuildPlayoutItems(
Playout playout,
DateTimeOffset playoutStart,
DateTimeOffset playoutFinish,
Map<CollectionKey, List<MediaItem>> collectionMediaItems)
{
DateTimeOffset trimBefore = playoutStart.AddHours(-4);
DateTimeOffset trimAfter = playoutFinish;
DateTimeOffset start = playoutStart;
DateTimeOffset finish = playoutStart.Date.AddDays(1);
_logger.LogDebug(
"Trim before: {TrimBefore}, Start: {Start}, Finish: {Finish}, PlayoutFinish: {PlayoutFinish}",
trimBefore,
start,
finish,
playoutFinish);
// build each day with "continue" anchors
while (finish < playoutFinish)
{
_logger.LogDebug("Building playout from {Start} to {Finish}", start, finish);
playout = await BuildPlayoutItems(playout, start, finish, collectionMediaItems, true);
start = playout.Anchor.NextStartOffset;
finish = finish.AddDays(1);
}
if (start < playoutFinish)
{
playout.Items.Clear();
playout.Anchor = null;
playout.ProgramScheduleAnchors.Clear();
// build one final time without continue anchors
_logger.LogDebug("Building final playout from {Start} to {Finish}", start, playoutFinish);
playout = await BuildPlayoutItems(
playout,
start,
playoutFinish,
collectionMediaItems,
false);
}
// remove any items outside the desired range
playout.Items.RemoveAll(old => old.FinishOffset < trimBefore || old.StartOffset > trimAfter);
return playout;
}
private async Task<Playout> BuildPlayoutItems(
Playout playout,
DateTimeOffset playoutStart,
DateTimeOffset playoutFinish,
Map<CollectionKey, List<MediaItem>> collectionMediaItems,
bool saveAnchorDate)
{
var sortedScheduleItems = playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList();
CollectionEnumeratorState scheduleItemsEnumeratorState =
playout.Anchor?.ScheduleItemsEnumeratorState ?? new CollectionEnumeratorState
@ -122,13 +344,13 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -122,13 +344,13 @@ public class PlayoutBuilder : IPlayoutBuilder
// start at the previously-decided time
DateTimeOffset currentTime = startAnchor.NextStartOffset.ToLocalTime();
_logger.LogDebug(
"Starting playout {PlayoutId} for channel {ChannelNumber} - {ChannelName} at {StartTime}",
playout.Id,
playout.Channel.Number,
playout.Channel.Name,
currentTime);
// _logger.LogDebug(
// "Starting playout ({PlayoutId}) for channel {ChannelNumber} - {ChannelName} at {StartTime}",
// playout.Id,
// playout.Channel.Number,
// playout.Channel.Name,
// currentTime);
// removing any items scheduled past the start anchor
// this could happen if the app was closed after scheduling items
// but before saving the anchor
@ -204,13 +426,6 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -204,13 +426,6 @@ public class PlayoutBuilder : IPlayoutBuilder
// once more to get playout anchor
ProgramScheduleItem anchorScheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Current;
// build program schedule anchors
playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators);
// remove any items outside the desired range
playout.Items.RemoveAll(
old => old.FinishOffset < playoutStart.AddHours(-4) || old.StartOffset > playoutFinish);
if (playout.Items.Any())
{
DateTimeOffset maxStartTime = playout.Items.Max(i => i.FinishOffset);
@ -223,19 +438,27 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -223,19 +438,27 @@ public class PlayoutBuilder : IPlayoutBuilder
playout.Anchor = new PlayoutAnchor
{
ScheduleItemsEnumeratorState = playoutBuilderState.ScheduleItemsEnumerator.State,
NextStart = PlayoutModeSchedulerBase<ProgramScheduleItem>.GetStartTimeAfter(playoutBuilderState, anchorScheduleItem)
NextStart = PlayoutModeSchedulerBase<ProgramScheduleItem>
.GetStartTimeAfter(playoutBuilderState, anchorScheduleItem)
.UtcDateTime,
MultipleRemaining = playoutBuilderState.MultipleRemaining.IsSome
? playoutBuilderState.MultipleRemaining.ValueUnsafe()
: null,
DurationFinish = playoutBuilderState.DurationFinish.IsSome
? playoutBuilderState.DurationFinish.ValueUnsafe().UtcDateTime
: null,
InFlood = playoutBuilderState.InFlood,
InDurationFiller = playoutBuilderState.InDurationFiller,
NextGuideGroup = playoutBuilderState.NextGuideGroup
};
foreach (int multipleRemaining in playoutBuilderState.MultipleRemaining)
{
playout.Anchor.MultipleRemaining = multipleRemaining;
}
foreach (DateTimeOffset durationFinish in playoutBuilderState.DurationFinish)
{
playout.Anchor.DurationFinish = durationFinish.UtcDateTime;
}
// build program schedule anchors
playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators, saveAnchorDate);
return playout;
}
@ -311,7 +534,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -311,7 +534,7 @@ public class PlayoutBuilder : IPlayoutBuilder
return collectionMediaItems.Find(c => !c.Value.Any()).Map(c => c.Key);
}
private static PlayoutAnchor FindStartAnchor(
Playout playout,
DateTimeOffset start,
@ -341,7 +564,8 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -341,7 +564,8 @@ public class PlayoutBuilder : IPlayoutBuilder
private static List<PlayoutProgramScheduleAnchor> BuildProgramScheduleAnchors(
Playout playout,
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators)
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
bool saveAnchorDate)
{
var result = new List<PlayoutProgramScheduleAnchor>();
@ -350,7 +574,8 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -350,7 +574,8 @@ public class PlayoutBuilder : IPlayoutBuilder
Option<PlayoutProgramScheduleAnchor> maybeExisting = playout.ProgramScheduleAnchors.FirstOrDefault(
a => a.CollectionType == collectionKey.CollectionType
&& a.CollectionId == collectionKey.CollectionId
&& a.MediaItemId == collectionKey.MediaItemId);
&& a.MediaItemId == collectionKey.MediaItemId
&& a.AnchorDate is null);
var maybeEnumeratorState = collectionEnumerators.GroupBy(e => e.Key, e => e.Value.State).ToDictionary(
mcs => mcs.Key,
@ -376,9 +601,20 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -376,9 +601,20 @@ public class PlayoutBuilder : IPlayoutBuilder
EnumeratorState = maybeEnumeratorState[collectionKey]
});
if (saveAnchorDate)
{
scheduleAnchor.AnchorDate = playout.Anchor?.NextStart;
}
result.Add(scheduleAnchor);
}
foreach (PlayoutProgramScheduleAnchor continueAnchor in playout.ProgramScheduleAnchors.Where(
a => a.AnchorDate is not null))
{
result.Add(continueAnchor);
}
return result;
}
@ -416,7 +652,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -416,7 +652,7 @@ public class PlayoutBuilder : IPlayoutBuilder
state);
}
}
switch (playbackOrder)
{
case PlaybackOrder.Chronological:

8
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs

@ -74,9 +74,9 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche @@ -74,9 +74,9 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
: FillerKind.None,
CustomTitle = scheduleItem.CustomTitle
};
durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime);
durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime);
DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc);
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
collectionEnumerators,
@ -192,7 +192,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche @@ -192,7 +192,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
}
nextState = nextState with { NextGuideGroup = nextState.IncrementGuideGroup };
return Tuple(nextState, playoutItems);
}
}

8
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs

@ -73,7 +73,7 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul @@ -73,7 +73,7 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
{
playoutItems.AddRange(
AddFiller(nextState, collectionEnumerators, scheduleItem, playoutItem, itemChapters));
LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
DateTimeOffset actualEndTime = playoutItems.Max(p => p.FinishOffset);
if (Math.Abs((itemEndTimeWithFiller - actualEndTime).TotalSeconds) > 1)
@ -97,9 +97,9 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul @@ -97,9 +97,9 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
}
}
_logger.LogDebug(
"Advancing to next schedule item after playout mode {PlayoutMode}",
"Flood");
// _logger.LogDebug(
// "Advancing to next schedule item after playout mode {PlayoutMode}",
// "Flood");
nextState = nextState with
{

8
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

@ -24,7 +24,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche @@ -24,7 +24,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
DateTimeOffset hardStop)
{
var playoutItems = new List<PlayoutItem>();
PlayoutBuilderState nextState = playoutBuilderState with
{
MultipleRemaining = playoutBuilderState.MultipleRemaining.IfNone(scheduleItem.Count)
@ -74,7 +74,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche @@ -74,7 +74,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
playoutItems.AddRange(
AddFiller(nextState, collectionEnumerators, scheduleItem, playoutItem, itemChapters));
nextState = nextState with
{
CurrentTime = itemEndTimeWithFiller,
@ -99,7 +99,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche @@ -99,7 +99,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
nextState.ScheduleItemsEnumerator.MoveNext();
}
DateTimeOffset nextItemStart = GetStartTimeAfter(nextState, nextScheduleItem);
if (scheduleItem.TailFiller != null)
@ -121,7 +121,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche @@ -121,7 +121,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
playoutItems,
nextItemStart);
}
nextState = nextState with { NextGuideGroup = nextState.IncrementGuideGroup };
return Tuple(nextState, playoutItems);

12
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs

@ -57,11 +57,6 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI @@ -57,11 +57,6 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
playoutItem,
itemChapters);
// only play one item from collection, so always advance to the next item
// _logger.LogDebug(
// "Advancing to next schedule item after playout mode {PlayoutMode}",
// "One");
PlayoutBuilderState nextState = playoutBuilderState with
{
CurrentTime = itemEndTimeWithFiller
@ -72,6 +67,11 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI @@ -72,6 +67,11 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
// only play one item from collection, so always advance to the next item
// _logger.LogDebug(
// "Advancing to next schedule item after playout mode {PlayoutMode}",
// "One");
DateTimeOffset nextItemStart = GetStartTimeAfter(nextState, nextScheduleItem);
if (scheduleItem.TailFiller != null)
@ -98,7 +98,7 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI @@ -98,7 +98,7 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
return Tuple(nextState, playoutItems);
}
return Tuple(playoutBuilderState, new List<PlayoutItem>());
}
}

8
ErsatzTV.Core/Scheduling/PlayoutParameters.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Scheduling;
public record PlayoutParameters(
DateTimeOffset Start,
DateTimeOffset Finish,
Map<CollectionKey, List<MediaItem>> CollectionMediaItems);

3
ErsatzTV.Infrastructure/Data/Configurations/PlayoutProgramScheduleAnchorConfiguration.cs

@ -29,5 +29,8 @@ public class PlayoutProgramScheduleAnchorConfiguration : IEntityTypeConfiguratio @@ -29,5 +29,8 @@ public class PlayoutProgramScheduleAnchorConfiguration : IEntityTypeConfiguratio
.HasForeignKey(i => i.MediaItemId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
builder.Property(i => i.AnchorDate)
.IsRequired(false);
}
}

3864
ErsatzTV.Infrastructure/Migrations/20220209084802_Add_PlayoutProgramScheduleAnchor_AnchorDate.Designer.cs generated

File diff suppressed because it is too large Load Diff

26
ErsatzTV.Infrastructure/Migrations/20220209084802_Add_PlayoutProgramScheduleAnchor_AnchorDate.cs

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_PlayoutProgramScheduleAnchor_AnchorDate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "AnchorDate",
table: "PlayoutProgramScheduleAnchor",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AnchorDate",
table: "PlayoutProgramScheduleAnchor");
}
}
}

3
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -1346,6 +1346,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1346,6 +1346,9 @@ namespace ErsatzTV.Infrastructure.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime?>("AnchorDate")
.HasColumnType("TEXT");
b.Property<int?>("CollectionId")
.HasColumnType("INTEGER");

36
ErsatzTV/Pages/Playouts.razor

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
@using ErsatzTV.Application.Playouts
@using ErsatzTV.Application.Configuration
@implements IDisposable
@using ErsatzTV.Core.Scheduling
@inject IDialogService _dialog
@inject IMediator _mediator
@ -44,14 +45,14 @@ @@ -44,14 +45,14 @@
@* <MudTd DataLabel="Playout Type">@context.ProgramSchedulePlayoutType</MudTd> *@
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Rebuild Playout">
<MudTooltip Text="Reset Playout">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
OnClick="@(_ => RebuildPlayout(context))">
OnClick="@(_ => ResetPlayout(context))">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Schedule Rebuild">
<MudTooltip Text="Schedule Reset">
<MudIconButton Icon="@Icons.Material.Filled.Update"
OnClick="@(_ => ScheduleRebuild(context))">
OnClick="@(_ => ScheduleReset(context))">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Playout">
@ -114,11 +115,14 @@ @@ -114,11 +115,14 @@
if (_showFiller != value)
{
_showFiller = value;
_detailTable.ReloadServerData();
if (_selectedPlayoutId != null)
{
_detailTable.ReloadServerData();
}
}
}
}
public void Dispose()
{
_cts.Cancel();
@ -141,14 +145,6 @@ @@ -141,14 +145,6 @@
await _detailTable.ReloadServerData();
}
private async Task ShowFillerChanged()
{
if (_selectedPlayoutId != null)
{
await _detailTable.ReloadServerData();
}
}
private async Task DeletePlayout(PlayoutNameViewModel playout)
{
var parameters = new DialogParameters { { "EntityType", "playout" }, { "EntityName", $"{playout.ScheduleName} on {playout.ChannelNumber} - {playout.ChannelName}" } };
@ -167,9 +163,9 @@ @@ -167,9 +163,9 @@
}
}
private async Task RebuildPlayout(PlayoutNameViewModel playout)
private async Task ResetPlayout(PlayoutNameViewModel playout)
{
await _mediator.Send(new BuildPlayout(playout.PlayoutId, true), _cts.Token);
await _mediator.Send(new BuildPlayout(playout.PlayoutId, PlayoutBuildMode.Reset), _cts.Token);
await _table.ReloadServerData();
if (_selectedPlayoutId == playout.PlayoutId)
{
@ -177,19 +173,21 @@ @@ -177,19 +173,21 @@
}
}
private async Task ScheduleRebuild(PlayoutNameViewModel playout)
private async Task ScheduleReset(PlayoutNameViewModel playout)
{
var parameters = new DialogParameters
{
{ "PlayoutId", playout.PlayoutId },
{ "ChannelName", playout.ChannelName },
{ "ScheduleName", playout.ScheduleName },
{ "DailyRebuildTime", playout.DailyRebuildTime }
{ "DailyResetTime", playout.DailyRebuildTime }
};
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = _dialog.Show<SchedulePlayoutRebuild>("Schedule Playout Rebuild", parameters, options);
IDialogReference dialog = _dialog.Show<SchedulePlayoutReset>("Schedule Playout Reset", parameters, options);
await dialog.Result;
await _table.ReloadServerData();
}
private async Task<TableData<PlayoutNameViewModel>> ServerReload(TableState state)

2
ErsatzTV/Program.cs

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using Destructurama;
using ErsatzTV.Core;
using Serilog;
@ -35,6 +36,7 @@ public class Program @@ -35,6 +36,7 @@ public class Program
{
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(Configuration)
.Destructure.UsingAttributes()
.Enrich.FromLogContext()
.WriteTo.SQLite(FileSystemLayout.LogDatabasePath, retentionPeriod: TimeSpan.FromDays(1))
.WriteTo.File(FileSystemLayout.LogFilePath, rollingInterval: RollingInterval.Day)

20
ErsatzTV/Services/SchedulerService.cs

@ -9,6 +9,7 @@ using ErsatzTV.Application.Plex; @@ -9,6 +9,7 @@ using ErsatzTV.Application.Plex;
using ErsatzTV.Application.Search;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
@ -39,7 +40,7 @@ public class SchedulerService : BackgroundService @@ -39,7 +40,7 @@ public class SchedulerService : BackgroundService
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
DateTime firstRun = DateTime.Now;
// run once immediately at startup
if (!cancellationToken.IsCancellationRequested)
{
@ -66,9 +67,10 @@ public class SchedulerService : BackgroundService @@ -66,9 +67,10 @@ public class SchedulerService : BackgroundService
var roundedMinute = (int)(Math.Round(DateTime.Now.Minute / 5.0) * 5);
if (roundedMinute % 30 == 0)
{
// check for playouts to rebuild every 30 minutes
await RebuildPlayouts(cancellationToken);
// check for playouts to reset every 30 minutes
await ResetPlayouts(cancellationToken);
}
if (roundedMinute % 60 == 0 && DateTime.Now.Subtract(firstRun) > TimeSpan.FromHours(1))
{
// do other work every hour (on the hour)
@ -112,7 +114,7 @@ public class SchedulerService : BackgroundService @@ -112,7 +114,7 @@ public class SchedulerService : BackgroundService
}
}
private async Task RebuildPlayouts(CancellationToken cancellationToken)
private async Task ResetPlayouts(CancellationToken cancellationToken)
{
try
{
@ -129,14 +131,16 @@ public class SchedulerService : BackgroundService @@ -129,14 +131,16 @@ public class SchedulerService : BackgroundService
if (DateTime.Now.Subtract(DateTime.Today.Add(playout.DailyRebuildTime ?? TimeSpan.FromDays(7))) <
TimeSpan.FromMinutes(5))
{
await _workerChannel.WriteAsync(new BuildPlayout(playout.Id, true), cancellationToken);
await _workerChannel.WriteAsync(
new BuildPlayout(playout.Id, PlayoutBuildMode.Reset),
cancellationToken);
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error during scheduler run");
try
{
using (IServiceScope scope = _serviceScopeFactory.CreateScope())
@ -162,7 +166,9 @@ public class SchedulerService : BackgroundService @@ -162,7 +166,9 @@ public class SchedulerService : BackgroundService
.ToListAsync(cancellationToken);
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id))
{
await _workerChannel.WriteAsync(new BuildPlayout(playoutId), cancellationToken);
await _workerChannel.WriteAsync(
new BuildPlayout(playoutId, PlayoutBuildMode.Continue),
cancellationToken);
}
}

20
ErsatzTV/Shared/SchedulePlayoutRebuild.razor → ErsatzTV/Shared/SchedulePlayoutReset.razor

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
@implements IDisposable
@inject IMediator _mediator
@inject ISnackbar _snackbar
@inject ILogger<SchedulePlayoutRebuild> _logger
@inject ILogger<SchedulePlayoutReset> _logger
<MudDialog>
<DialogContent>
@ -12,8 +12,8 @@ @@ -12,8 +12,8 @@
@FormatText()
</MudText>
</MudContainer>
<MudSelect Class="mb-6 mx-4" Label="Daily Rebuild Time" @bind-Value="_rebuildTime">
<MudSelectItem Value="@(Option<TimeSpan>.None)">Do not automatically rebuild</MudSelectItem>
<MudSelect Class="mb-6 mx-4" Label="Daily Reset Time" @bind-Value="_resetTime">
<MudSelectItem Value="@(Option<TimeSpan>.None)">Do not automatically reset</MudSelectItem>
@for (var i = 1; i < 48; i++)
{
var time = TimeSpan.FromHours(i * 0.5);
@ -39,7 +39,7 @@ @@ -39,7 +39,7 @@
[Parameter]
public int PlayoutId { get; set; }
[Parameter]
public string ChannelName { get; set; }
@ -47,16 +47,16 @@ @@ -47,16 +47,16 @@
public string ScheduleName { get; set; }
[Parameter]
public Option<TimeSpan> DailyRebuildTime { get; set; }
public Option<TimeSpan> DailyResetTime { get; set; }
private string FormatText() => $"Enter the time that the playout on channel {ChannelName} with schedule {ScheduleName} should rebuild every day";
private string FormatText() => $"Enter the time that the playout on channel {ChannelName} with schedule {ScheduleName} should reset every day";
private record DummyModel;
private readonly DummyModel _dummyModel = new();
private Option<TimeSpan> _rebuildTime;
private Option<TimeSpan> _resetTime;
public void Dispose()
{
_cts.Cancel();
@ -65,13 +65,13 @@ @@ -65,13 +65,13 @@
protected override void OnParametersSet()
{
_rebuildTime = DailyRebuildTime;
_resetTime = DailyResetTime;
}
private async Task Submit()
{
Either<BaseError, PlayoutNameViewModel> maybeResult =
await _mediator.Send(new UpdatePlayout(PlayoutId, _rebuildTime), _cts.Token);
await _mediator.Send(new UpdatePlayout(PlayoutId, _resetTime), _cts.Token);
maybeResult.Match(
playout => { MudDialog.Close(DialogResult.Ok(playout)); },
Loading…
Cancel
Save