Browse Source

add "on demand" channel progress mode (#1790)

* update dependencies

* add channel progress mode

* implement on demand channel progress

* update changelog
pull/1791/head
Jason Dove 1 year ago committed by GitHub
parent
commit
a8b658a5ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      CHANGELOG.md
  2. 1
      ErsatzTV.Application/Channels/ChannelViewModel.cs
  3. 1
      ErsatzTV.Application/Channels/Commands/CreateChannel.cs
  4. 1
      ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
  5. 1
      ErsatzTV.Application/Channels/Commands/UpdateChannel.cs
  6. 1
      ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
  7. 1
      ErsatzTV.Application/Channels/Mapper.cs
  8. 6
      ErsatzTV.Application/ErsatzTV.Application.csproj
  9. 4
      ErsatzTV.Application/Playouts/Commands/CreateFloodPlayoutHandler.cs
  10. 6
      ErsatzTV.Application/Playouts/Commands/TimeShiftOnDemandPlayout.cs
  11. 41
      ErsatzTV.Application/Playouts/Commands/TimeShiftOnDemandPlayoutHandler.cs
  12. 3
      ErsatzTV.Application/Playouts/Commands/UpdateExternalJsonPlayoutHandler.cs
  13. 4
      ErsatzTV.Application/Playouts/Commands/UpdateOnDemandCheckpoint.cs
  14. 62
      ErsatzTV.Application/Playouts/Commands/UpdateOnDemandCheckpointHandler.cs
  15. 3
      ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs
  16. 6
      ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs
  17. 3
      ErsatzTV.Application/Playouts/Queries/GetAllPlayoutsHandler.cs
  18. 17
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  19. 17
      ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs
  20. 6
      ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
  21. 19
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  22. 2
      ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs
  23. 1
      ErsatzTV.Core/Domain/Channel.cs
  24. 7
      ErsatzTV.Core/Domain/ChannelProgressMode.cs
  25. 1
      ErsatzTV.Core/Domain/Playout.cs
  26. 10
      ErsatzTV.Core/ErsatzTV.Core.csproj
  27. 8
      ErsatzTV.Core/Interfaces/Scheduling/IPlayoutTimeShifter.cs
  28. 54
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  29. 90
      ErsatzTV.Core/Scheduling/PlayoutTimeShifter.cs
  30. 2
      ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj
  31. 2
      ErsatzTV.Infrastructure.MySql/ErsatzTV.Infrastructure.MySql.csproj
  32. 5802
      ErsatzTV.Infrastructure.MySql/Migrations/20240716141648_Add_Channel_ProgressMode.Designer.cs
  33. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20240716141648_Add_Channel_ProgressMode.cs
  34. 5805
      ErsatzTV.Infrastructure.MySql/Migrations/20240716155846_Add_Playout_OnDemandCheckpoint.Designer.cs
  35. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20240716155846_Add_Playout_OnDemandCheckpoint.cs
  36. 8
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  37. 4
      ErsatzTV.Infrastructure.Sqlite/ErsatzTV.Infrastructure.Sqlite.csproj
  38. 5641
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240716135729_Add_Channel_ProgressMode.Designer.cs
  39. 29
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240716135729_Add_Channel_ProgressMode.cs
  40. 5644
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240716150019_Add_Playout_OnDemandCheckpoint.Designer.cs
  41. 29
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240716150019_Add_Playout_OnDemandCheckpoint.cs
  42. 8
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  43. 16
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  44. 2
      ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj
  45. 12
      ErsatzTV.Scanner/ErsatzTV.Scanner.csproj
  46. 24
      ErsatzTV/ErsatzTV.csproj
  47. 5
      ErsatzTV/Pages/ChannelEditor.razor
  48. 24
      ErsatzTV/Pages/Playouts.razor
  49. 3
      ErsatzTV/Services/WorkerService.cs
  50. 2
      ErsatzTV/Startup.cs
  51. 3
      ErsatzTV/ViewModels/ChannelEditViewModel.cs

7
CHANGELOG.md

@ -9,6 +9,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- These libraries will now appear as ETV Other Video libraries - These libraries will now appear as ETV Other Video libraries
- Items in these libraries will have tag metadata added from folders just like local Other Video libraries - Items in these libraries will have tag metadata added from folders just like local Other Video libraries
- Thanks @raknam for adding this feature! - Thanks @raknam for adding this feature!
- Add *experimental* support for `On Demand` channel progress
- With `On Demand` channel progress, the schedule will only advance when the channel is being streamed
- When the channel is idle, the schedule is unmodified and will be shifted forward as needed so no content is missed
- Setting a channel to `On Demand` progress will disable alternate schedules
- The `On Demand` setting will only be used for `Flood` playouts (NOT `Block` or `External JSON`)
- It is NOT recommended to use fixed start times with `On Demand` progress
- This will probably be disabled with a future update
### Fixed ### Fixed
- Add basic cache busting to XMLTV image URLs - Add basic cache busting to XMLTV image URLs

1
ErsatzTV.Application/Channels/ChannelViewModel.cs

@ -12,6 +12,7 @@ public record ChannelViewModel(
string Logo, string Logo,
string PreferredAudioLanguageCode, string PreferredAudioLanguageCode,
string PreferredAudioTitle, string PreferredAudioTitle,
ChannelProgressMode ProgressMode,
StreamingMode StreamingMode, StreamingMode StreamingMode,
int? WatermarkId, int? WatermarkId,
int? FallbackFillerId, int? FallbackFillerId,

1
ErsatzTV.Application/Channels/Commands/CreateChannel.cs

@ -12,6 +12,7 @@ public record CreateChannel(
string Logo, string Logo,
string PreferredAudioLanguageCode, string PreferredAudioLanguageCode,
string PreferredAudioTitle, string PreferredAudioTitle,
ChannelProgressMode ProgressMode,
StreamingMode StreamingMode, StreamingMode StreamingMode,
int? WatermarkId, int? WatermarkId,
int? FallbackFillerId, int? FallbackFillerId,

1
ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs

@ -73,6 +73,7 @@ public class CreateChannelHandler(
Group = request.Group, Group = request.Group,
Categories = request.Categories, Categories = request.Categories,
FFmpegProfileId = ffmpegProfileId, FFmpegProfileId = ffmpegProfileId,
ProgressMode = request.ProgressMode,
StreamingMode = request.StreamingMode, StreamingMode = request.StreamingMode,
Artwork = artwork, Artwork = artwork,
PreferredAudioLanguageCode = preferredAudioLanguageCode, PreferredAudioLanguageCode = preferredAudioLanguageCode,

1
ErsatzTV.Application/Channels/Commands/UpdateChannel.cs

@ -13,6 +13,7 @@ public record UpdateChannel(
string Logo, string Logo,
string PreferredAudioLanguageCode, string PreferredAudioLanguageCode,
string PreferredAudioTitle, string PreferredAudioTitle,
ChannelProgressMode ProgressMode,
StreamingMode StreamingMode, StreamingMode StreamingMode,
int? WatermarkId, int? WatermarkId,
int? FallbackFillerId, int? FallbackFillerId,

1
ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs

@ -67,6 +67,7 @@ public class UpdateChannelHandler(
}); });
} }
c.ProgressMode = update.ProgressMode;
c.StreamingMode = update.StreamingMode; c.StreamingMode = update.StreamingMode;
c.WatermarkId = update.WatermarkId; c.WatermarkId = update.WatermarkId;
c.FallbackFillerId = update.FallbackFillerId; c.FallbackFillerId = update.FallbackFillerId;

1
ErsatzTV.Application/Channels/Mapper.cs

@ -16,6 +16,7 @@ internal static class Mapper
GetLogo(channel), GetLogo(channel),
channel.PreferredAudioLanguageCode, channel.PreferredAudioLanguageCode,
channel.PreferredAudioTitle, channel.PreferredAudioTitle,
channel.ProgressMode,
channel.StreamingMode, channel.StreamingMode,
channel.WatermarkId, channel.WatermarkId,
channel.FallbackFillerId, channel.FallbackFillerId,

6
ErsatzTV.Application/ErsatzTV.Application.csproj

@ -12,7 +12,7 @@
<PackageReference Include="Bugsnag" Version="3.1.0" /> <PackageReference Include="Bugsnag" Version="3.1.0" />
<PackageReference Include="CliWrap" Version="3.6.6" /> <PackageReference Include="CliWrap" Version="3.6.6" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" /> <PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="MediatR" Version="12.2.0" /> <PackageReference Include="MediatR" Version="12.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.10.48"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.10.48">
@ -20,8 +20,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="3.0.0" /> <PackageReference Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
<PackageReference Include="WebMarkupMin.Core" Version="2.16.0" /> <PackageReference Include="WebMarkupMin.Core" Version="2.16.2" />
<PackageReference Include="Winista.MimeDetect" Version="1.1.0" /> <PackageReference Include="Winista.MimeDetect" Version="1.1.0" />
</ItemGroup> </ItemGroup>

4
ErsatzTV.Application/Playouts/Commands/CreateFloodPlayoutHandler.cs

@ -37,6 +37,10 @@ public class CreateFloodPlayoutHandler : IRequestHandler<CreateFloodPlayout, Eit
await dbContext.Playouts.AddAsync(playout); await dbContext.Playouts.AddAsync(playout);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset)); await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset));
if (playout.Channel.ProgressMode is ChannelProgressMode.OnDemand)
{
await _channel.WriteAsync(new TimeShiftOnDemandPlayout(playout.Channel.Number, DateTimeOffset.Now, false));
}
await _channel.WriteAsync(new RefreshChannelList()); await _channel.WriteAsync(new RefreshChannelList());
return new CreatePlayoutResponse(playout.Id); return new CreatePlayoutResponse(playout.Id);
} }

6
ErsatzTV.Application/Playouts/Commands/TimeShiftOnDemandPlayout.cs

@ -0,0 +1,6 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Playouts;
public record TimeShiftOnDemandPlayout(string ChannelNumber, DateTimeOffset Now, bool Force)
: IRequest<Option<BaseError>>, IBackgroundServiceRequest;

41
ErsatzTV.Application/Playouts/Commands/TimeShiftOnDemandPlayoutHandler.cs

@ -0,0 +1,41 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Playouts;
public class TimeShiftOnDemandPlayoutHandler(
IPlayoutTimeShifter playoutTimeShifter,
IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<TimeShiftOnDemandPlayout, Option<BaseError>>
{
public async Task<Option<BaseError>> Handle(TimeShiftOnDemandPlayout request, CancellationToken cancellationToken)
{
try
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Playout> maybePlayout = await dbContext.Playouts
.Include(p => p.Channel)
.Include(p => p.Items)
.Include(p => p.Anchor)
.Include(p => p.ProgramScheduleAnchors)
.SelectOneAsync(p => p.Channel.Number, p => p.Channel.Number == request.ChannelNumber);
foreach (Playout playout in maybePlayout)
{
playoutTimeShifter.TimeShift(playout, request.Now, request.Force);
await dbContext.SaveChangesAsync(cancellationToken);
}
return Option<BaseError>.None;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
}

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

@ -49,9 +49,10 @@ public class
playout.ProgramSchedulePlayoutType, playout.ProgramSchedulePlayoutType,
playout.Channel.Name, playout.Channel.Name,
playout.Channel.Number, playout.Channel.Number,
playout.Channel.ProgressMode,
playout.ProgramSchedule?.Name ?? string.Empty, playout.ProgramSchedule?.Name ?? string.Empty,
playout.ExternalJsonFile, playout.ExternalJsonFile,
Optional(playout.DailyRebuildTime)); playout.DailyRebuildTime);
} }
private static Task<Validation<BaseError, Playout>> Validate( private static Task<Validation<BaseError, Playout>> Validate(

4
ErsatzTV.Application/Playouts/Commands/UpdateOnDemandCheckpoint.cs

@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Playouts;
public record UpdateOnDemandCheckpoint(string ChannelNumber, DateTimeOffset Checkpoint)
: IRequest;

62
ErsatzTV.Application/Playouts/Commands/UpdateOnDemandCheckpointHandler.cs

@ -0,0 +1,62 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Playouts;
public class UpdateOnDemandCheckpointHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ILogger<UpdateOnDemandCheckpointHandler> logger)
: IRequestHandler<UpdateOnDemandCheckpoint>
{
public async Task Handle(UpdateOnDemandCheckpoint request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Playout> maybePlayout = await dbContext.Playouts
.Include(p => p.Channel)
.Include(p => p.Items)
.SelectOneAsync(p => p.Channel.Number, p => p.Channel.Number == request.ChannelNumber);
foreach (Playout playout in maybePlayout)
{
if (playout.Channel.ProgressMode is not ChannelProgressMode.OnDemand)
{
return;
}
int timeout = await (await configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout))
.IfNoneAsync(60);
// don't move checkpoint back in time
DateTimeOffset newCheckpoint = request.Checkpoint - TimeSpan.FromSeconds(timeout);
if (newCheckpoint > playout.OnDemandCheckpoint)
{
playout.OnDemandCheckpoint = newCheckpoint;
}
// don't checkpoint before the first item
// this could happen if you watch a new playout for less time than the segmenter timeout
if (playout.Items.Count > 0)
{
DateTimeOffset minStart = playout.Items.Min(p => p.StartOffset);
if (playout.OnDemandCheckpoint < minStart)
{
playout.OnDemandCheckpoint = minStart;
}
}
logger.LogDebug(
"Updating on demand checkpoint for channel {Number} - {Name} to {Checkpoint}",
playout.Channel.Number,
playout.Channel.Name,
playout.OnDemandCheckpoint);
await dbContext.SaveChangesAsync(cancellationToken);
}
}
}

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

@ -41,9 +41,10 @@ public class UpdatePlayoutHandler : IRequestHandler<UpdatePlayout, Either<BaseEr
playout.ProgramSchedulePlayoutType, playout.ProgramSchedulePlayoutType,
playout.Channel.Name, playout.Channel.Name,
playout.Channel.Number, playout.Channel.Number,
playout.Channel.ProgressMode,
playout.ProgramSchedule?.Name ?? string.Empty, playout.ProgramSchedule?.Name ?? string.Empty,
playout.ExternalJsonFile, playout.ExternalJsonFile,
Optional(playout.DailyRebuildTime)); playout.DailyRebuildTime);
} }
private static Task<Validation<BaseError, Playout>> Validate(TvContext dbContext, UpdatePlayout request) => private static Task<Validation<BaseError, Playout>> Validate(TvContext dbContext, UpdatePlayout request) =>

6
ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs

@ -7,6 +7,10 @@ public record PlayoutNameViewModel(
ProgramSchedulePlayoutType PlayoutType, ProgramSchedulePlayoutType PlayoutType,
string ChannelName, string ChannelName,
string ChannelNumber, string ChannelNumber,
ChannelProgressMode ProgressMode,
string ScheduleName, string ScheduleName,
string ExternalJsonFile, string ExternalJsonFile,
Option<TimeSpan> DailyRebuildTime); TimeSpan? DbDailyRebuildTime)
{
public Option<TimeSpan> DailyRebuildTime => Optional(DbDailyRebuildTime);
}

3
ErsatzTV.Application/Playouts/Queries/GetAllPlayoutsHandler.cs

@ -25,9 +25,10 @@ public class GetAllPlayoutsHandler : IRequestHandler<GetAllPlayouts, List<Playou
p.ProgramSchedulePlayoutType, p.ProgramSchedulePlayoutType,
p.Channel.Name, p.Channel.Name,
p.Channel.Number, p.Channel.Number,
p.Channel.ProgressMode,
p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name, p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name,
p.ExternalJsonFile, p.ExternalJsonFile,
Optional(p.DailyRebuildTime))) p.DailyRebuildTime))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
} }
} }

17
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -6,6 +6,7 @@ using System.Timers;
using Bugsnag; using Bugsnag;
using CliWrap; using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;
using ErsatzTV.Application.Playouts;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.FFmpeg;
@ -172,6 +173,9 @@ public class HlsSessionWorker : IHlsSessionWorker
_transcodedUntil = DateTimeOffset.Now; _transcodedUntil = DateTimeOffset.Now;
PlaylistStart = _transcodedUntil; PlaylistStart = _transcodedUntil;
// time shift on-demand playout if needed
await _mediator.Send(new TimeShiftOnDemandPlayout(_channelNumber, _transcodedUntil, true), cancellationToken);
bool initialWorkAhead = Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit(); bool initialWorkAhead = Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit();
_state = initialWorkAhead ? HlsSessionState.SeekAndWorkAhead : HlsSessionState.SeekAndRealtime; _state = initialWorkAhead ? HlsSessionState.SeekAndWorkAhead : HlsSessionState.SeekAndRealtime;
@ -236,7 +240,7 @@ public class HlsSessionWorker : IHlsSessionWorker
} }
catch (Exception) catch (Exception)
{ {
// do nothing // do nothing
} }
} }
} }
@ -524,6 +528,17 @@ public class HlsSessionWorker : IHlsSessionWorker
} }
finally finally
{ {
try
{
await _mediator.Send(
new UpdateOnDemandCheckpoint(_channelNumber, DateTimeOffset.Now),
CancellationToken.None);
}
catch (Exception)
{
// do nothing
}
if (!realtime) if (!realtime)
{ {
Interlocked.Decrement(ref _workAheadCount); Interlocked.Decrement(ref _workAheadCount);

17
ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs

@ -4,6 +4,7 @@ using System.Text;
using System.Timers; using System.Timers;
using CliWrap; using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;
using ErsatzTV.Application.Playouts;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
@ -124,6 +125,9 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker
_transcodedUntil = DateTimeOffset.Now; _transcodedUntil = DateTimeOffset.Now;
PlaylistStart = _transcodedUntil; PlaylistStart = _transcodedUntil;
// time shift on-demand playout if needed
await _mediator.Send(new TimeShiftOnDemandPlayout(_channelNumber, _transcodedUntil, true), cancellationToken);
// start concat/segmenter process // start concat/segmenter process
// other transcode processes will be started by incoming requests from concat/segmenter process // other transcode processes will be started by incoming requests from concat/segmenter process
@ -171,6 +175,17 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker
_timer.Elapsed -= CancelRun; _timer.Elapsed -= CancelRun;
} }
try
{
await _mediator.Send(
new UpdateOnDemandCheckpoint(_channelNumber, DateTimeOffset.Now),
CancellationToken.None);
}
catch (Exception)
{
// do nothing
}
try try
{ {
_localFileSystem.EmptyFolder(Path.Combine(FileSystemLayout.TranscodeFolder, _channelNumber)); _localFileSystem.EmptyFolder(Path.Combine(FileSystemLayout.TranscodeFolder, _channelNumber));
@ -192,7 +207,7 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker
} }
catch (Exception) catch (Exception)
{ {
// do nothing // do nothing
} }
} }
} }

6
ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj

@ -10,7 +10,7 @@
<PackageReference Include="Bugsnag" Version="3.1.0" /> <PackageReference Include="Bugsnag" Version="3.1.0" />
<PackageReference Include="CliWrap" Version="3.6.6" /> <PackageReference Include="CliWrap" Version="3.6.6" />
<PackageReference Include="FluentAssertions" Version="6.12.0" /> <PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="LanguageExt.Core" Version="4.4.8" /> <PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
@ -24,9 +24,9 @@
<PackageReference Include="NSubstitute" Version="5.1.0" /> <PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="NUnit" Version="4.1.0" /> <PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" /> <PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

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

@ -558,6 +558,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -657,6 +658,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -804,6 +806,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -909,6 +912,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1023,6 +1027,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1130,6 +1135,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1241,6 +1247,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1357,6 +1364,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1462,6 +1470,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1578,6 +1587,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1705,6 +1715,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1824,6 +1835,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1903,6 +1915,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -2118,6 +2131,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -2609,6 +2623,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -2723,6 +2738,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -2837,6 +2853,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -2946,6 +2963,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
collectionRepo, collectionRepo,
televisionRepo, televisionRepo,
@ -3001,6 +3019,7 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
collectionRepo, collectionRepo,
televisionRepo, televisionRepo,

2
ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs

@ -117,6 +117,7 @@ public class ScheduleIntegrationTests
provider.GetRequiredService<IFallbackMetadataProvider>()); provider.GetRequiredService<IFallbackMetadataProvider>());
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
new ConfigElementRepository(factory), new ConfigElementRepository(factory),
new MediaCollectionRepository(Substitute.For<IClient>(), searchIndex, factory), new MediaCollectionRepository(Substitute.For<IClient>(), searchIndex, factory),
new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()), new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()),
@ -287,6 +288,7 @@ public class ScheduleIntegrationTests
DateTimeOffset finish = start.AddDays(2); DateTimeOffset finish = start.AddDays(2);
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
new ConfigElementRepository(factory), new ConfigElementRepository(factory),
new MediaCollectionRepository(Substitute.For<IClient>(), Substitute.For<ISearchIndex>(), factory), new MediaCollectionRepository(Substitute.For<IClient>(), Substitute.For<ISearchIndex>(), factory),
new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()), new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()),

1
ErsatzTV.Core/Domain/Channel.cs

@ -28,4 +28,5 @@ public class Channel
public ChannelSubtitleMode SubtitleMode { get; set; } public ChannelSubtitleMode SubtitleMode { get; set; }
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; } public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
public string MusicVideoCreditsTemplate { get; set; } public string MusicVideoCreditsTemplate { get; set; }
public ChannelProgressMode ProgressMode { get; set; }
} }

7
ErsatzTV.Core/Domain/ChannelProgressMode.cs

@ -0,0 +1,7 @@
namespace ErsatzTV.Core.Domain;
public enum ChannelProgressMode
{
Always = 0,
OnDemand = 1
}

1
ErsatzTV.Core/Domain/Playout.cs

@ -22,4 +22,5 @@ public class Playout
public TimeSpan? DailyRebuildTime { get; set; } public TimeSpan? DailyRebuildTime { get; set; }
public int? DecoId { get; set; } public int? DecoId { get; set; }
public Deco Deco { get; set; } public Deco Deco { get; set; }
public DateTimeOffset? OnDemandCheckpoint { get; set; }
} }

10
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -12,21 +12,21 @@
<PackageReference Include="Bugsnag" Version="3.1.0" /> <PackageReference Include="Bugsnag" Version="3.1.0" />
<PackageReference Include="Destructurama.Attributed" Version="4.0.0" /> <PackageReference Include="Destructurama.Attributed" Version="4.0.0" />
<PackageReference Include="Flurl" Version="4.0.0" /> <PackageReference Include="Flurl" Version="4.0.0" />
<PackageReference Include="LanguageExt.Core" Version="4.4.8" /> <PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="LanguageExt.Transformers" Version="4.4.8" /> <PackageReference Include="LanguageExt.Transformers" Version="4.4.8" />
<PackageReference Include="MediatR" Version="12.2.0" /> <PackageReference Include="MediatR" Version="12.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" /> <PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.10.48"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.10.48">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

8
ErsatzTV.Core/Interfaces/Scheduling/IPlayoutTimeShifter.cs

@ -0,0 +1,8 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IPlayoutTimeShifter
{
public void TimeShift(Playout playout, DateTimeOffset now, bool force);
}

54
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -17,6 +17,7 @@ public class PlayoutBuilder : IPlayoutBuilder
{ {
private static readonly Random Random = new(); private static readonly Random Random = new();
private readonly IArtistRepository _artistRepository; private readonly IArtistRepository _artistRepository;
private readonly IPlayoutTimeShifter _playoutTimeShifter;
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
@ -26,6 +27,7 @@ public class PlayoutBuilder : IPlayoutBuilder
private ILogger<PlayoutBuilder> _logger; private ILogger<PlayoutBuilder> _logger;
public PlayoutBuilder( public PlayoutBuilder(
IPlayoutTimeShifter playoutTimeShifter,
IConfigElementRepository configElementRepository, IConfigElementRepository configElementRepository,
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository, ITelevisionRepository televisionRepository,
@ -34,6 +36,7 @@ public class PlayoutBuilder : IPlayoutBuilder
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
ILogger<PlayoutBuilder> logger) ILogger<PlayoutBuilder> logger)
{ {
_playoutTimeShifter = playoutTimeShifter;
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
@ -79,6 +82,12 @@ public class PlayoutBuilder : IPlayoutBuilder
// return await Build(playout, mode, parameters with { Start = parameters.Start.AddDays(-2) }); // return await Build(playout, mode, parameters with { Start = parameters.Start.AddDays(-2) });
// } // }
// time shift on demand channel if needed
if (playout.Channel.ProgressMode is ChannelProgressMode.OnDemand && mode is not PlayoutBuildMode.Reset)
{
_playoutTimeShifter.TimeShift(playout, parameters.Start, false);
}
return await Build(playout, mode, parameters, cancellationToken); return await Build(playout, mode, parameters, cancellationToken);
} }
@ -238,6 +247,13 @@ public class PlayoutBuilder : IPlayoutBuilder
playout.Items.Clear(); playout.Items.Clear();
playout.Anchor = null; playout.Anchor = null;
playout.ProgramScheduleAnchors.Clear(); playout.ProgramScheduleAnchors.Clear();
playout.OnDemandCheckpoint = null;
// don't trim start for on demand channels, we want to time shift it all forward
if (playout.Channel.ProgressMode is ChannelProgressMode.OnDemand)
{
TrimStart = false;
}
await BuildPlayoutItems( await BuildPlayoutItems(
playout, playout,
@ -247,6 +263,12 @@ public class PlayoutBuilder : IPlayoutBuilder
true, true,
cancellationToken); cancellationToken);
// time shift on demand channel if needed
if (playout.Channel.ProgressMode is ChannelProgressMode.OnDemand)
{
_playoutTimeShifter.TimeShift(playout, parameters.Start, false);
}
return playout; return playout;
} }
@ -391,20 +413,24 @@ public class PlayoutBuilder : IPlayoutBuilder
playout.Items.RemoveAll(old => old.FinishOffset < trimBefore); playout.Items.RemoveAll(old => old.FinishOffset < trimBefore);
} }
// check for future items that aren't grouped inside range // on demand channels end up with slightly more than expected due to time shifting from midnight to first build
var futureItems = playout.Items.Filter(i => i.StartOffset > trimAfter).ToList(); if (playout.Channel.ProgressMode is not ChannelProgressMode.OnDemand)
foreach (PlayoutItem futureItem in futureItems)
{ {
if (playout.Items.All(i => i == futureItem || i.GuideGroup != futureItem.GuideGroup)) // check for future items that aren't grouped inside range
var futureItems = playout.Items.Filter(i => i.StartOffset > trimAfter).ToList();
foreach (PlayoutItem futureItem in futureItems)
{ {
_logger.LogError( if (playout.Items.All(i => i == futureItem || i.GuideGroup != futureItem.GuideGroup))
"Playout item scheduled for {Time} after hard stop of {HardStop}", {
futureItem.StartOffset, _logger.LogError(
trimAfter); "Playout item scheduled for {Time} after hard stop of {HardStop}",
futureItem.StartOffset,
trimAfter);
// it feels hacky to have to clean up a playlist like this, // it feels hacky to have to clean up a playlist like this,
// so only log the error, and leave the bad data to fail tests // so only log the error, and leave the bad data to fail tests
// playout.Items.Remove(futureItem); // playout.Items.Remove(futureItem);
}
} }
} }
@ -425,6 +451,12 @@ public class PlayoutBuilder : IPlayoutBuilder
playout.ProgramScheduleAlternates, playout.ProgramScheduleAlternates,
playoutStart); playoutStart);
// on demand channels do NOT use alternate schedules
if (playout.Channel.ProgressMode is ChannelProgressMode.OnDemand)
{
activeSchedule = playout.ProgramSchedule;
}
// _logger.LogDebug("Active schedule is: {Schedule}", activeSchedule.Name); // _logger.LogDebug("Active schedule is: {Schedule}", activeSchedule.Name);
// random start points are disabled in some scenarios, so ensure it's enabled and active // random start points are disabled in some scenarios, so ensure it's enabled and active

90
ErsatzTV.Core/Scheduling/PlayoutTimeShifter.cs

@ -0,0 +1,90 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Scheduling;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling;
public class PlayoutTimeShifter(IFFmpegSegmenterService segmenterService, ILogger<PlayoutTimeShifter> logger)
: IPlayoutTimeShifter
{
public void TimeShift(Playout playout, DateTimeOffset now, bool force)
{
if (playout.Channel.ProgressMode is not ChannelProgressMode.OnDemand)
{
return;
}
if (!force && segmenterService.IsActive(playout.Channel.Number))
{
logger.LogDebug(
"Will not time shift on demand playout that is active for channel {Number} - {Name}",
playout.Channel.Number,
playout.Channel.Name);
return;
}
if (playout.Items.Count == 0)
{
logger.LogDebug(
"Unable to time shift empty playout for channel {Number} - {Name}",
playout.Channel.Number,
playout.Channel.Name);
return;
}
if (playout.OnDemandCheckpoint is null)
{
logger.LogDebug(
"Time shifting unwatched playout for channel {Number} - {Name}",
playout.Channel.Number,
playout.Channel.Name);
playout.OnDemandCheckpoint = playout.Items.Min(p => p.StartOffset);
}
TimeSpan toOffset = now - playout.OnDemandCheckpoint.IfNone(now);
logger.LogDebug(
"Time shifting playout for channel {Number} - {Name} forward by {Time}",
playout.Channel.Number,
playout.Channel.Name,
toOffset);
// time shift items
foreach (PlayoutItem playoutItem in playout.Items)
{
playoutItem.Start += toOffset;
playoutItem.Finish += toOffset;
if (playoutItem.GuideStart.HasValue)
{
playoutItem.GuideStart += toOffset;
}
if (playoutItem.GuideFinish.HasValue)
{
playoutItem.GuideFinish += toOffset;
}
}
// time shift anchors
foreach (PlayoutProgramScheduleAnchor anchor in playout.ProgramScheduleAnchors)
{
if (anchor.AnchorDate.HasValue)
{
anchor.AnchorDate += toOffset;
}
}
// time shift anchor
if (playout.Anchor is not null)
{
playout.Anchor.NextStart += toOffset;
}
playout.OnDemandCheckpoint = now;
}
}

2
ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CliWrap" Version="3.6.6" /> <PackageReference Include="CliWrap" Version="3.6.6" />
<PackageReference Include="LanguageExt.Core" Version="4.4.8" /> <PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
</ItemGroup> </ItemGroup>

2
ErsatzTV.Infrastructure.MySql/ErsatzTV.Infrastructure.MySql.csproj

@ -16,7 +16,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
</ItemGroup> </ItemGroup>

5802
ErsatzTV.Infrastructure.MySql/Migrations/20240716141648_Add_Channel_ProgressMode.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20240716141648_Add_Channel_ProgressMode.cs

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_Channel_ProgressMode : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "ProgressMode",
table: "Channel",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ProgressMode",
table: "Channel");
}
}
}

5805
ErsatzTV.Infrastructure.MySql/Migrations/20240716155846_Add_Playout_OnDemandCheckpoint.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20240716155846_Add_Playout_OnDemandCheckpoint.cs

@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_Playout_OnDemandCheckpoint : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTimeOffset>(
name: "OnDemandCheckpoint",
table: "Playout",
type: "datetime(6)",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "OnDemandCheckpoint",
table: "Playout");
}
}
}

8
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -17,7 +17,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "8.0.5") .HasAnnotation("ProductVersion", "8.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64); .HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
@ -277,6 +277,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<string>("PreferredSubtitleLanguageCode") b.Property<string>("PreferredSubtitleLanguageCode")
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<int>("ProgressMode")
.HasColumnType("int");
b.Property<int>("StreamingMode") b.Property<int>("StreamingMode")
.HasColumnType("int"); .HasColumnType("int");
@ -1699,6 +1702,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<string>("ExternalJsonFile") b.Property<string>("ExternalJsonFile")
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<DateTimeOffset?>("OnDemandCheckpoint")
.HasColumnType("datetime(6)");
b.Property<int?>("ProgramScheduleId") b.Property<int?>("ProgramScheduleId")
.HasColumnType("int"); .HasColumnType("int");

4
ErsatzTV.Infrastructure.Sqlite/ErsatzTV.Infrastructure.Sqlite.csproj

@ -13,8 +13,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="2.1.35" /> <PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
</ItemGroup> </ItemGroup>

5641
ErsatzTV.Infrastructure.Sqlite/Migrations/20240716135729_Add_Channel_ProgressMode.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.Sqlite/Migrations/20240716135729_Add_Channel_ProgressMode.cs

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_Channel_ProgressMode : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "ProgressMode",
table: "Channel",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ProgressMode",
table: "Channel");
}
}
}

5644
ErsatzTV.Infrastructure.Sqlite/Migrations/20240716150019_Add_Playout_OnDemandCheckpoint.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.Sqlite/Migrations/20240716150019_Add_Playout_OnDemandCheckpoint.cs

@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_Playout_OnDemandCheckpoint : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTimeOffset>(
name: "OnDemandCheckpoint",
table: "Playout",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "OnDemandCheckpoint",
table: "Playout");
}
}
}

8
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.5"); modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b => modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
{ {
@ -264,6 +264,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<string>("PreferredSubtitleLanguageCode") b.Property<string>("PreferredSubtitleLanguageCode")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("ProgressMode")
.HasColumnType("INTEGER");
b.Property<int>("StreamingMode") b.Property<int>("StreamingMode")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -1612,6 +1615,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<string>("ExternalJsonFile") b.Property<string>("ExternalJsonFile")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<DateTimeOffset?>("OnDemandCheckpoint")
.HasColumnType("TEXT");
b.Property<int?>("ProgramScheduleId") b.Property<int?>("ProgramScheduleId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

16
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -13,24 +13,24 @@
<PackageReference Include="Blurhash.ImageSharp" Version="3.0.0" /> <PackageReference Include="Blurhash.ImageSharp" Version="3.0.0" />
<PackageReference Include="CliWrap" Version="3.6.6" /> <PackageReference Include="CliWrap" Version="3.6.6" />
<PackageReference Include="Dapper" Version="2.1.35" /> <PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.13.12" /> <PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.14.6" />
<PackageReference Include="Jint" Version="3.1.1" /> <PackageReference Include="Jint" Version="3.1.5" />
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00016" /> <PackageReference Include="Lucene.Net" Version="4.8.0-beta00016" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00016" /> <PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00016" />
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016" /> <PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.10.48"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.10.48">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Refit" Version="7.0.0" /> <PackageReference Include="Refit" Version="7.1.2" />
<PackageReference Include="Refit.Newtonsoft.Json" Version="7.0.0" /> <PackageReference Include="Refit.Newtonsoft.Json" Version="7.1.2" />
<PackageReference Include="Refit.Xml" Version="7.0.0" /> <PackageReference Include="Refit.Xml" Version="7.1.2" />
<PackageReference Include="Scriban.Signed" Version="5.10.0" /> <PackageReference Include="Scriban.Signed" Version="5.10.0" />
<PackageReference Include="TagLibSharp" Version="2.3.0" /> <PackageReference Include="TagLibSharp" Version="2.3.0" />
</ItemGroup> </ItemGroup>

2
ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" /> <PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="LanguageExt.Core" Version="4.4.8" /> <PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" /> <PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="NUnit" Version="4.1.0" /> <PackageReference Include="NUnit" Version="4.1.0" />

12
ErsatzTV.Scanner/ErsatzTV.Scanner.csproj

@ -22,17 +22,17 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CliWrap" Version="3.6.6" /> <PackageReference Include="CliWrap" Version="3.6.6" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" /> <PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="LanguageExt.Core" Version="4.4.8" /> <PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="MediatR" Version="12.2.0" /> <PackageReference Include="MediatR" Version="12.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" /> <PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup> </ItemGroup>

24
ErsatzTV/ErsatzTV.csproj

@ -16,20 +16,20 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Blazored.FluentValidation" Version="2.1.0" /> <PackageReference Include="Blazored.FluentValidation" Version="2.2.0" />
<PackageReference Include="Bugsnag.AspNet.Core" Version="3.1.0" /> <PackageReference Include="Bugsnag.AspNet.Core" Version="3.1.0" />
<PackageReference Include="FluentValidation" Version="11.9.1" /> <PackageReference Include="FluentValidation" Version="11.9.2" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" /> <PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="Heron.MudCalendar" Version="1.1.2" /> <PackageReference Include="Heron.MudCalendar" Version="1.1.2" />
<PackageReference Include="HtmlSanitizer" Version="8.0.865" /> <PackageReference Include="HtmlSanitizer" Version="8.0.865" />
<PackageReference Include="LanguageExt.Core" Version="4.4.8" /> <PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Markdig" Version="0.37.0" /> <PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" /> <PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.5" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -37,12 +37,12 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MudBlazor" Version="6.19.1" /> <PackageReference Include="MudBlazor" Version="6.21.0" />
<PackageReference Include="NaturalSort.Extension" Version="4.3.0" /> <PackageReference Include="NaturalSort.Extension" Version="4.3.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="7.0.0" /> <PackageReference Include="Refit.HttpClientFactory" Version="7.1.2" />
<PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.2" />
<PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" /> <PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
<PackageReference Include="System.Text.Encoding.Extensions" Version="4.3.0" /> <PackageReference Include="System.Text.Encoding.Extensions" Version="4.3.0" />
<PackageReference Include="System.Runtime.Handles" Version="4.3.0" /> <PackageReference Include="System.Runtime.Handles" Version="4.3.0" />

5
ErsatzTV/Pages/ChannelEditor.razor

@ -30,6 +30,10 @@
<MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/> <MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/>
<MudTextField Class="mt-3" Label="Group" @bind-Value="_model.Group" For="@(() => _model.Group)"/> <MudTextField Class="mt-3" Label="Group" @bind-Value="_model.Group" For="@(() => _model.Group)"/>
<MudTextField Class="mt-3" Label="Categories" @bind-Value="_model.Categories" For="@(() => _model.Categories)" Placeholder="Comma-separated list of categories"/> <MudTextField Class="mt-3" Label="Categories" @bind-Value="_model.Categories" For="@(() => _model.Categories)" Placeholder="Comma-separated list of categories"/>
<MudSelect Class="mt-3" Label="Progress Mode" @bind-Value="_model.ProgressMode" For="@(() => _model.ProgressMode)">
<MudSelectItem Value="@(ChannelProgressMode.Always)">Always</MudSelectItem>
<MudSelectItem Value="@(ChannelProgressMode.OnDemand)">On Demand</MudSelectItem>
</MudSelect>
<MudSelect Class="mt-3" Label="Streaming Mode" @bind-Value="_model.StreamingMode" For="@(() => _model.StreamingMode)"> <MudSelect Class="mt-3" Label="Streaming Mode" @bind-Value="_model.StreamingMode" For="@(() => _model.StreamingMode)">
<MudSelectItem Value="@(StreamingMode.TransportStreamHybrid)">MPEG-TS</MudSelectItem> <MudSelectItem Value="@(StreamingMode.TransportStreamHybrid)">MPEG-TS</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.TransportStream)">MPEG-TS (Legacy)</MudSelectItem> <MudSelectItem Value="@(StreamingMode.TransportStream)">MPEG-TS (Legacy)</MudSelectItem>
@ -180,6 +184,7 @@
_model.Number = channelViewModel.Number; _model.Number = channelViewModel.Number;
_model.FFmpegProfileId = channelViewModel.FFmpegProfileId; _model.FFmpegProfileId = channelViewModel.FFmpegProfileId;
_model.Logo = channelViewModel.Logo; _model.Logo = channelViewModel.Logo;
_model.ProgressMode = channelViewModel.ProgressMode;
_model.StreamingMode = channelViewModel.StreamingMode; _model.StreamingMode = channelViewModel.StreamingMode;
_model.PreferredAudioLanguageCode = channelViewModel.PreferredAudioLanguageCode; _model.PreferredAudioLanguageCode = channelViewModel.PreferredAudioLanguageCode;
_model.PreferredAudioTitle = channelViewModel.PreferredAudioTitle; _model.PreferredAudioTitle = channelViewModel.PreferredAudioTitle;

24
ErsatzTV/Pages/Playouts.razor

@ -81,12 +81,24 @@
</div> </div>
@if (context.PlayoutType == ProgramSchedulePlayoutType.Flood) @if (context.PlayoutType == ProgramSchedulePlayoutType.Flood)
{ {
<MudTooltip Text="Edit Alternate Schedules"> if (context.ProgressMode is ChannelProgressMode.OnDemand)
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar" {
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)" <MudTooltip Text="Alternate Schedules are not supported with On Demand progress">
Href="@($"playouts/{context.PlayoutId}/alternate-schedules")"> <MudIconButton Icon="@Icons.Material.Filled.EditCalendar"
</MudIconButton> Disabled="true">
</MudTooltip> </MudIconButton>
</MudTooltip>
}
else
{
<MudTooltip Text="Edit Alternate Schedules">
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
Href="@($"playouts/{context.PlayoutId}/alternate-schedules")">
</MudIconButton>
</MudTooltip>
}
<MudTooltip Text="Reset Playout"> <MudTooltip Text="Reset Playout">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" <MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)" Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"

3
ErsatzTV/Services/WorkerService.cs

@ -71,6 +71,9 @@ public class WorkerService : BackgroundService
buildPlayout.PlayoutId, buildPlayout.PlayoutId,
error.Value)); error.Value));
break; break;
case TimeShiftOnDemandPlayout timeShiftOnDemandPlayout:
await mediator.Send(timeShiftOnDemandPlayout, stoppingToken);
break;
case DeleteOrphanedArtwork deleteOrphanedArtwork: case DeleteOrphanedArtwork deleteOrphanedArtwork:
await mediator.Send(deleteOrphanedArtwork, stoppingToken); await mediator.Send(deleteOrphanedArtwork, stoppingToken);
break; break;

2
ErsatzTV/Startup.cs

@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Net.Security;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@ -654,6 +655,7 @@ public class Startup
services.AddScoped<IBlockPlayoutBuilder, BlockPlayoutBuilder>(); services.AddScoped<IBlockPlayoutBuilder, BlockPlayoutBuilder>();
services.AddScoped<IBlockPlayoutPreviewBuilder, BlockPlayoutPreviewBuilder>(); services.AddScoped<IBlockPlayoutPreviewBuilder, BlockPlayoutPreviewBuilder>();
services.AddScoped<IExternalJsonPlayoutBuilder, ExternalJsonPlayoutBuilder>(); services.AddScoped<IExternalJsonPlayoutBuilder, ExternalJsonPlayoutBuilder>();
services.AddScoped<IPlayoutTimeShifter, PlayoutTimeShifter>();
services.AddScoped<IImageCache, ImageCache>(); services.AddScoped<IImageCache, ImageCache>();
services.AddScoped<ILocalFileSystem, LocalFileSystem>(); services.AddScoped<ILocalFileSystem, LocalFileSystem>();
services.AddScoped<IPlexServerApiClient, PlexServerApiClient>(); services.AddScoped<IPlexServerApiClient, PlexServerApiClient>();

3
ErsatzTV/ViewModels/ChannelEditViewModel.cs

@ -15,6 +15,7 @@ public class ChannelEditViewModel
public string PreferredAudioLanguageCode { get; set; } public string PreferredAudioLanguageCode { get; set; }
public string PreferredAudioTitle { get; set; } public string PreferredAudioTitle { get; set; }
public string Logo { get; set; } public string Logo { get; set; }
public ChannelProgressMode ProgressMode { get; set; }
public StreamingMode StreamingMode { get; set; } public StreamingMode StreamingMode { get; set; }
public int? WatermarkId { get; set; } public int? WatermarkId { get; set; }
public int? FallbackFillerId { get; set; } public int? FallbackFillerId { get; set; }
@ -41,6 +42,7 @@ public class ChannelEditViewModel
Logo, Logo,
PreferredAudioLanguageCode, PreferredAudioLanguageCode,
PreferredAudioTitle, PreferredAudioTitle,
ProgressMode,
StreamingMode, StreamingMode,
WatermarkId, WatermarkId,
FallbackFillerId, FallbackFillerId,
@ -59,6 +61,7 @@ public class ChannelEditViewModel
Logo, Logo,
PreferredAudioLanguageCode, PreferredAudioLanguageCode,
PreferredAudioTitle, PreferredAudioTitle,
ProgressMode,
StreamingMode, StreamingMode,
WatermarkId, WatermarkId,
FallbackFillerId, FallbackFillerId,

Loading…
Cancel
Save