add "on demand" channel progress mode (#1790)
* update dependencies * add channel progress mode * implement on demand channel progress * update changelog
This commit is contained in:
@@ -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
|
||||
- Items in these libraries will have tag metadata added from folders just like local Other Video libraries
|
||||
- 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
|
||||
- Add basic cache busting to XMLTV image URLs
|
||||
|
||||
@@ -12,6 +12,7 @@ public record ChannelViewModel(
|
||||
string Logo,
|
||||
string PreferredAudioLanguageCode,
|
||||
string PreferredAudioTitle,
|
||||
ChannelProgressMode ProgressMode,
|
||||
StreamingMode StreamingMode,
|
||||
int? WatermarkId,
|
||||
int? FallbackFillerId,
|
||||
|
||||
@@ -12,6 +12,7 @@ public record CreateChannel(
|
||||
string Logo,
|
||||
string PreferredAudioLanguageCode,
|
||||
string PreferredAudioTitle,
|
||||
ChannelProgressMode ProgressMode,
|
||||
StreamingMode StreamingMode,
|
||||
int? WatermarkId,
|
||||
int? FallbackFillerId,
|
||||
|
||||
@@ -73,6 +73,7 @@ public class CreateChannelHandler(
|
||||
Group = request.Group,
|
||||
Categories = request.Categories,
|
||||
FFmpegProfileId = ffmpegProfileId,
|
||||
ProgressMode = request.ProgressMode,
|
||||
StreamingMode = request.StreamingMode,
|
||||
Artwork = artwork,
|
||||
PreferredAudioLanguageCode = preferredAudioLanguageCode,
|
||||
|
||||
@@ -13,6 +13,7 @@ public record UpdateChannel(
|
||||
string Logo,
|
||||
string PreferredAudioLanguageCode,
|
||||
string PreferredAudioTitle,
|
||||
ChannelProgressMode ProgressMode,
|
||||
StreamingMode StreamingMode,
|
||||
int? WatermarkId,
|
||||
int? FallbackFillerId,
|
||||
|
||||
@@ -67,6 +67,7 @@ public class UpdateChannelHandler(
|
||||
});
|
||||
}
|
||||
|
||||
c.ProgressMode = update.ProgressMode;
|
||||
c.StreamingMode = update.StreamingMode;
|
||||
c.WatermarkId = update.WatermarkId;
|
||||
c.FallbackFillerId = update.FallbackFillerId;
|
||||
|
||||
@@ -16,6 +16,7 @@ internal static class Mapper
|
||||
GetLogo(channel),
|
||||
channel.PreferredAudioLanguageCode,
|
||||
channel.PreferredAudioTitle,
|
||||
channel.ProgressMode,
|
||||
channel.StreamingMode,
|
||||
channel.WatermarkId,
|
||||
channel.FallbackFillerId,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<PackageReference Include="Bugsnag" Version="3.1.0" />
|
||||
<PackageReference Include="CliWrap" Version="3.6.6" />
|
||||
<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.Hosting.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.10.48">
|
||||
@@ -20,8 +20,8 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="3.0.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.16.0" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.16.2" />
|
||||
<PackageReference Include="Winista.MimeDetect" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -37,6 +37,10 @@ public class CreateFloodPlayoutHandler : IRequestHandler<CreateFloodPlayout, Eit
|
||||
await dbContext.Playouts.AddAsync(playout);
|
||||
await dbContext.SaveChangesAsync();
|
||||
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());
|
||||
return new CreatePlayoutResponse(playout.Id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts;
|
||||
|
||||
public record TimeShiftOnDemandPlayout(string ChannelNumber, DateTimeOffset Now, bool Force)
|
||||
: IRequest<Option<BaseError>>, IBackgroundServiceRequest;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,9 +49,10 @@ public class
|
||||
playout.ProgramSchedulePlayoutType,
|
||||
playout.Channel.Name,
|
||||
playout.Channel.Number,
|
||||
playout.Channel.ProgressMode,
|
||||
playout.ProgramSchedule?.Name ?? string.Empty,
|
||||
playout.ExternalJsonFile,
|
||||
Optional(playout.DailyRebuildTime));
|
||||
playout.DailyRebuildTime);
|
||||
}
|
||||
|
||||
private static Task<Validation<BaseError, Playout>> Validate(
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.Playouts;
|
||||
|
||||
public record UpdateOnDemandCheckpoint(string ChannelNumber, DateTimeOffset Checkpoint)
|
||||
: IRequest;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,9 +41,10 @@ public class UpdatePlayoutHandler : IRequestHandler<UpdatePlayout, Either<BaseEr
|
||||
playout.ProgramSchedulePlayoutType,
|
||||
playout.Channel.Name,
|
||||
playout.Channel.Number,
|
||||
playout.Channel.ProgressMode,
|
||||
playout.ProgramSchedule?.Name ?? string.Empty,
|
||||
playout.ExternalJsonFile,
|
||||
Optional(playout.DailyRebuildTime));
|
||||
playout.DailyRebuildTime);
|
||||
}
|
||||
|
||||
private static Task<Validation<BaseError, Playout>> Validate(TvContext dbContext, UpdatePlayout request) =>
|
||||
|
||||
@@ -7,6 +7,10 @@ public record PlayoutNameViewModel(
|
||||
ProgramSchedulePlayoutType PlayoutType,
|
||||
string ChannelName,
|
||||
string ChannelNumber,
|
||||
ChannelProgressMode ProgressMode,
|
||||
string ScheduleName,
|
||||
string ExternalJsonFile,
|
||||
Option<TimeSpan> DailyRebuildTime);
|
||||
TimeSpan? DbDailyRebuildTime)
|
||||
{
|
||||
public Option<TimeSpan> DailyRebuildTime => Optional(DbDailyRebuildTime);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,10 @@ public class GetAllPlayoutsHandler : IRequestHandler<GetAllPlayouts, List<Playou
|
||||
p.ProgramSchedulePlayoutType,
|
||||
p.Channel.Name,
|
||||
p.Channel.Number,
|
||||
p.Channel.ProgressMode,
|
||||
p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name,
|
||||
p.ExternalJsonFile,
|
||||
Optional(p.DailyRebuildTime)))
|
||||
p.DailyRebuildTime))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Timers;
|
||||
using Bugsnag;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
@@ -172,6 +173,9 @@ public class HlsSessionWorker : IHlsSessionWorker
|
||||
_transcodedUntil = DateTimeOffset.Now;
|
||||
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();
|
||||
_state = initialWorkAhead ? HlsSessionState.SeekAndWorkAhead : HlsSessionState.SeekAndRealtime;
|
||||
|
||||
@@ -524,6 +528,17 @@ public class HlsSessionWorker : IHlsSessionWorker
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
await _mediator.Send(
|
||||
new UpdateOnDemandCheckpoint(_channelNumber, DateTimeOffset.Now),
|
||||
CancellationToken.None);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
if (!realtime)
|
||||
{
|
||||
Interlocked.Decrement(ref _workAheadCount);
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Text;
|
||||
using System.Timers;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
@@ -124,6 +125,9 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker
|
||||
_transcodedUntil = DateTimeOffset.Now;
|
||||
PlaylistStart = _transcodedUntil;
|
||||
|
||||
// time shift on-demand playout if needed
|
||||
await _mediator.Send(new TimeShiftOnDemandPlayout(_channelNumber, _transcodedUntil, true), cancellationToken);
|
||||
|
||||
// start 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;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _mediator.Send(
|
||||
new UpdateOnDemandCheckpoint(_channelNumber, DateTimeOffset.Now),
|
||||
CancellationToken.None);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_localFileSystem.EmptyFolder(Path.Combine(FileSystemLayout.TranscodeFolder, _channelNumber));
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<PackageReference Include="Bugsnag" Version="3.1.0" />
|
||||
<PackageReference Include="CliWrap" Version="3.6.6" />
|
||||
<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.DependencyInjection" 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="NUnit" Version="4.1.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.Sinks.Debug" Version="2.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -558,6 +558,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -657,6 +658,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -804,6 +806,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -909,6 +912,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -1023,6 +1027,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -1130,6 +1135,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -1241,6 +1247,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -1357,6 +1364,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -1462,6 +1470,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -1578,6 +1587,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -1705,6 +1715,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -1824,6 +1835,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -1903,6 +1915,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -2118,6 +2131,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -2609,6 +2623,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -2723,6 +2738,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -2837,6 +2853,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
@@ -2946,6 +2963,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
collectionRepo,
|
||||
televisionRepo,
|
||||
@@ -3001,6 +3019,7 @@ public class PlayoutBuilderTests
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
|
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
configRepo,
|
||||
collectionRepo,
|
||||
televisionRepo,
|
||||
|
||||
@@ -117,6 +117,7 @@ public class ScheduleIntegrationTests
|
||||
provider.GetRequiredService<IFallbackMetadataProvider>());
|
||||
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
new ConfigElementRepository(factory),
|
||||
new MediaCollectionRepository(Substitute.For<IClient>(), searchIndex, factory),
|
||||
new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()),
|
||||
@@ -287,6 +288,7 @@ public class ScheduleIntegrationTests
|
||||
DateTimeOffset finish = start.AddDays(2);
|
||||
|
||||
var builder = new PlayoutBuilder(
|
||||
Substitute.For<IPlayoutTimeShifter>(),
|
||||
new ConfigElementRepository(factory),
|
||||
new MediaCollectionRepository(Substitute.For<IClient>(), Substitute.For<ISearchIndex>(), factory),
|
||||
new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()),
|
||||
|
||||
@@ -28,4 +28,5 @@ public class Channel
|
||||
public ChannelSubtitleMode SubtitleMode { get; set; }
|
||||
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
|
||||
public string MusicVideoCreditsTemplate { get; set; }
|
||||
public ChannelProgressMode ProgressMode { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Core.Domain;
|
||||
|
||||
public enum ChannelProgressMode
|
||||
{
|
||||
Always = 0,
|
||||
OnDemand = 1
|
||||
}
|
||||
@@ -22,4 +22,5 @@ public class Playout
|
||||
public TimeSpan? DailyRebuildTime { get; set; }
|
||||
public int? DecoId { get; set; }
|
||||
public Deco Deco { get; set; }
|
||||
public DateTimeOffset? OnDemandCheckpoint { get; set; }
|
||||
}
|
||||
|
||||
@@ -12,21 +12,21 @@
|
||||
<PackageReference Include="Bugsnag" Version="3.1.0" />
|
||||
<PackageReference Include="Destructurama.Attributed" 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="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.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<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">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ public class PlayoutBuilder : IPlayoutBuilder
|
||||
{
|
||||
private static readonly Random Random = new();
|
||||
private readonly IArtistRepository _artistRepository;
|
||||
private readonly IPlayoutTimeShifter _playoutTimeShifter;
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
@@ -26,6 +27,7 @@ public class PlayoutBuilder : IPlayoutBuilder
|
||||
private ILogger<PlayoutBuilder> _logger;
|
||||
|
||||
public PlayoutBuilder(
|
||||
IPlayoutTimeShifter playoutTimeShifter,
|
||||
IConfigElementRepository configElementRepository,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ITelevisionRepository televisionRepository,
|
||||
@@ -34,6 +36,7 @@ public class PlayoutBuilder : IPlayoutBuilder
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILogger<PlayoutBuilder> logger)
|
||||
{
|
||||
_playoutTimeShifter = playoutTimeShifter;
|
||||
_configElementRepository = configElementRepository;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_televisionRepository = televisionRepository;
|
||||
@@ -79,6 +82,12 @@ public class PlayoutBuilder : IPlayoutBuilder
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -238,6 +247,13 @@ public class PlayoutBuilder : IPlayoutBuilder
|
||||
playout.Items.Clear();
|
||||
playout.Anchor = null;
|
||||
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(
|
||||
playout,
|
||||
@@ -247,6 +263,12 @@ public class PlayoutBuilder : IPlayoutBuilder
|
||||
true,
|
||||
cancellationToken);
|
||||
|
||||
// time shift on demand channel if needed
|
||||
if (playout.Channel.ProgressMode is ChannelProgressMode.OnDemand)
|
||||
{
|
||||
_playoutTimeShifter.TimeShift(playout, parameters.Start, false);
|
||||
}
|
||||
|
||||
return playout;
|
||||
}
|
||||
|
||||
@@ -391,6 +413,9 @@ public class PlayoutBuilder : IPlayoutBuilder
|
||||
playout.Items.RemoveAll(old => old.FinishOffset < trimBefore);
|
||||
}
|
||||
|
||||
// on demand channels end up with slightly more than expected due to time shifting from midnight to first build
|
||||
if (playout.Channel.ProgressMode is not ChannelProgressMode.OnDemand)
|
||||
{
|
||||
// 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)
|
||||
@@ -407,6 +432,7 @@ public class PlayoutBuilder : IPlayoutBuilder
|
||||
// playout.Items.Remove(futureItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return playout;
|
||||
}
|
||||
@@ -425,6 +451,12 @@ public class PlayoutBuilder : IPlayoutBuilder
|
||||
playout.ProgramScheduleAlternates,
|
||||
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);
|
||||
|
||||
// random start points are disabled in some scenarios, so ensure it's enabled and active
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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.Logging.Abstractions" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</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" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Generated
+5802
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+5805
File diff suppressed because it is too large
Load Diff
+29
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.5")
|
||||
.HasAnnotation("ProductVersion", "8.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||
@@ -277,6 +277,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.Property<string>("PreferredSubtitleLanguageCode")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("ProgressMode")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("StreamingMode")
|
||||
.HasColumnType("int");
|
||||
|
||||
@@ -1699,6 +1702,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.Property<string>("ExternalJsonFile")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTimeOffset?>("OnDemandCheckpoint")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int?>("ProgramScheduleId")
|
||||
.HasColumnType("int");
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
Generated
+5641
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+5644
File diff suppressed because it is too large
Load Diff
+29
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.5");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
|
||||
{
|
||||
@@ -264,6 +264,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
b.Property<string>("PreferredSubtitleLanguageCode")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ProgressMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("StreamingMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -1612,6 +1615,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
b.Property<string>("ExternalJsonFile")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("OnDemandCheckpoint")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ProgramScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
@@ -13,24 +13,24 @@
|
||||
<PackageReference Include="Blurhash.ImageSharp" Version="3.0.0" />
|
||||
<PackageReference Include="CliWrap" Version="3.6.6" />
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.13.12" />
|
||||
<PackageReference Include="Jint" Version="3.1.1" />
|
||||
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.14.6" />
|
||||
<PackageReference Include="Jint" Version="3.1.5" />
|
||||
<PackageReference Include="Lucene.Net" 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="Microsoft.EntityFrameworkCore" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.5">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</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">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Refit" Version="7.0.0" />
|
||||
<PackageReference Include="Refit.Newtonsoft.Json" Version="7.0.0" />
|
||||
<PackageReference Include="Refit.Xml" Version="7.0.0" />
|
||||
<PackageReference Include="Refit" Version="7.1.2" />
|
||||
<PackageReference Include="Refit.Newtonsoft.Json" Version="7.1.2" />
|
||||
<PackageReference Include="Refit.Xml" Version="7.1.2" />
|
||||
<PackageReference Include="Scriban.Signed" Version="5.10.0" />
|
||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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="NSubstitute" Version="5.1.0" />
|
||||
<PackageReference Include="NUnit" Version="4.1.0" />
|
||||
|
||||
@@ -22,17 +22,17 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CliWrap" Version="3.6.6" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.4.8" />
|
||||
<PackageReference Include="MediatR" Version="12.2.0" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
|
||||
<PackageReference Include="MediatR" Version="12.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" 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="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
+12
-12
@@ -16,20 +16,20 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<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="FluentValidation" Version="11.9.1" />
|
||||
<PackageReference Include="FluentValidation" Version="11.9.2" />
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
||||
<PackageReference Include="Heron.MudCalendar" Version="1.1.2" />
|
||||
<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="MediatR.Courier.DependencyInjection" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.5">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
@@ -37,12 +37,12 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</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="Refit.HttpClientFactory" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Refit.HttpClientFactory" Version="7.1.2" />
|
||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||
<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.Text.Encoding.Extensions" Version="4.3.0" />
|
||||
<PackageReference Include="System.Runtime.Handles" Version="4.3.0" />
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
<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="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)">
|
||||
<MudSelectItem Value="@(StreamingMode.TransportStreamHybrid)">MPEG-TS</MudSelectItem>
|
||||
<MudSelectItem Value="@(StreamingMode.TransportStream)">MPEG-TS (Legacy)</MudSelectItem>
|
||||
@@ -180,6 +184,7 @@
|
||||
_model.Number = channelViewModel.Number;
|
||||
_model.FFmpegProfileId = channelViewModel.FFmpegProfileId;
|
||||
_model.Logo = channelViewModel.Logo;
|
||||
_model.ProgressMode = channelViewModel.ProgressMode;
|
||||
_model.StreamingMode = channelViewModel.StreamingMode;
|
||||
_model.PreferredAudioLanguageCode = channelViewModel.PreferredAudioLanguageCode;
|
||||
_model.PreferredAudioTitle = channelViewModel.PreferredAudioTitle;
|
||||
|
||||
@@ -80,6 +80,16 @@
|
||||
}
|
||||
</div>
|
||||
@if (context.PlayoutType == ProgramSchedulePlayoutType.Flood)
|
||||
{
|
||||
if (context.ProgressMode is ChannelProgressMode.OnDemand)
|
||||
{
|
||||
<MudTooltip Text="Alternate Schedules are not supported with On Demand progress">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar"
|
||||
Disabled="true">
|
||||
</MudIconButton>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="Edit Alternate Schedules">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar"
|
||||
@@ -87,6 +97,8 @@
|
||||
Href="@($"playouts/{context.PlayoutId}/alternate-schedules")">
|
||||
</MudIconButton>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
<MudTooltip Text="Reset Playout">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
||||
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
|
||||
|
||||
@@ -71,6 +71,9 @@ public class WorkerService : BackgroundService
|
||||
buildPlayout.PlayoutId,
|
||||
error.Value));
|
||||
break;
|
||||
case TimeShiftOnDemandPlayout timeShiftOnDemandPlayout:
|
||||
await mediator.Send(timeShiftOnDemandPlayout, stoppingToken);
|
||||
break;
|
||||
case DeleteOrphanedArtwork deleteOrphanedArtwork:
|
||||
await mediator.Send(deleteOrphanedArtwork, stoppingToken);
|
||||
break;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Net.Security;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
@@ -654,6 +655,7 @@ public class Startup
|
||||
services.AddScoped<IBlockPlayoutBuilder, BlockPlayoutBuilder>();
|
||||
services.AddScoped<IBlockPlayoutPreviewBuilder, BlockPlayoutPreviewBuilder>();
|
||||
services.AddScoped<IExternalJsonPlayoutBuilder, ExternalJsonPlayoutBuilder>();
|
||||
services.AddScoped<IPlayoutTimeShifter, PlayoutTimeShifter>();
|
||||
services.AddScoped<IImageCache, ImageCache>();
|
||||
services.AddScoped<ILocalFileSystem, LocalFileSystem>();
|
||||
services.AddScoped<IPlexServerApiClient, PlexServerApiClient>();
|
||||
|
||||
@@ -15,6 +15,7 @@ public class ChannelEditViewModel
|
||||
public string PreferredAudioLanguageCode { get; set; }
|
||||
public string PreferredAudioTitle { get; set; }
|
||||
public string Logo { get; set; }
|
||||
public ChannelProgressMode ProgressMode { get; set; }
|
||||
public StreamingMode StreamingMode { get; set; }
|
||||
public int? WatermarkId { get; set; }
|
||||
public int? FallbackFillerId { get; set; }
|
||||
@@ -41,6 +42,7 @@ public class ChannelEditViewModel
|
||||
Logo,
|
||||
PreferredAudioLanguageCode,
|
||||
PreferredAudioTitle,
|
||||
ProgressMode,
|
||||
StreamingMode,
|
||||
WatermarkId,
|
||||
FallbackFillerId,
|
||||
@@ -59,6 +61,7 @@ public class ChannelEditViewModel
|
||||
Logo,
|
||||
PreferredAudioLanguageCode,
|
||||
PreferredAudioTitle,
|
||||
ProgressMode,
|
||||
StreamingMode,
|
||||
WatermarkId,
|
||||
FallbackFillerId,
|
||||
|
||||
Reference in New Issue
Block a user