mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* replace playout externaljsonfile and templatefile with schedulefile * add scripted schedule-based playout * wip - not functional yet * temp disable scripted playout creation * allow fast-forwarding block playouts * add xmltv block behavior settingpull/2339/head
76 changed files with 25613 additions and 146 deletions
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Application.Configuration; |
||||
|
||||
public enum XmltvBlockBehavior |
||||
{ |
||||
SplitTimeEvenly = 0, |
||||
UseActualTimes = 1 |
||||
} |
||||
@ -0,0 +1,96 @@
@@ -0,0 +1,96 @@
|
||||
using System.Threading.Channels; |
||||
using ErsatzTV.Application.Channels; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Channel = ErsatzTV.Core.Domain.Channel; |
||||
|
||||
namespace ErsatzTV.Application.Playouts; |
||||
|
||||
public class CreateScriptedPlayoutHandler |
||||
: IRequestHandler<CreateScriptedPlayout, Either<BaseError, CreatePlayoutResponse>> |
||||
{ |
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
private readonly ILocalFileSystem _localFileSystem; |
||||
|
||||
public CreateScriptedPlayoutHandler( |
||||
ILocalFileSystem localFileSystem, |
||||
ChannelWriter<IBackgroundServiceRequest> channel, |
||||
IDbContextFactory<TvContext> dbContextFactory) |
||||
{ |
||||
_localFileSystem = localFileSystem; |
||||
_channel = channel; |
||||
_dbContextFactory = dbContextFactory; |
||||
} |
||||
|
||||
public async Task<Either<BaseError, CreatePlayoutResponse>> Handle( |
||||
CreateScriptedPlayout request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
Validation<BaseError, Playout> validation = await Validate(dbContext, request); |
||||
return await validation.Apply(playout => PersistPlayout(dbContext, playout)); |
||||
} |
||||
|
||||
private async Task<CreatePlayoutResponse> PersistPlayout(TvContext dbContext, Playout playout) |
||||
{ |
||||
await dbContext.Playouts.AddAsync(playout); |
||||
await dbContext.SaveChangesAsync(); |
||||
await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset)); |
||||
if (playout.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand) |
||||
{ |
||||
await _channel.WriteAsync(new TimeShiftOnDemandPlayout(playout.Id, DateTimeOffset.Now, false)); |
||||
} |
||||
|
||||
await _channel.WriteAsync(new RefreshChannelList()); |
||||
return new CreatePlayoutResponse(playout.Id); |
||||
} |
||||
|
||||
private async Task<Validation<BaseError, Playout>> Validate( |
||||
TvContext dbContext, |
||||
CreateScriptedPlayout request) => |
||||
(await ValidateChannel(dbContext, request), ValidateScheduleFile(request), ValidateScheduleKind(request)) |
||||
.Apply((channel, yamlFile, scheduleKind) => new Playout |
||||
{ |
||||
ChannelId = channel.Id, |
||||
ScheduleFile = yamlFile, |
||||
ScheduleKind = scheduleKind, |
||||
Seed = new Random().Next() |
||||
}); |
||||
|
||||
private static Task<Validation<BaseError, Channel>> ValidateChannel( |
||||
TvContext dbContext, |
||||
CreateScriptedPlayout createScriptedPlayout) => |
||||
dbContext.Channels |
||||
.Include(c => c.Playouts) |
||||
.SelectOneAsync(c => c.Id, c => c.Id == createScriptedPlayout.ChannelId) |
||||
.Map(o => o.ToValidation<BaseError>("Channel does not exist")) |
||||
.BindT(ChannelMustNotHavePlayouts); |
||||
|
||||
private static Validation<BaseError, Channel> ChannelMustNotHavePlayouts(Channel channel) => |
||||
Optional(channel.Playouts.Count) |
||||
.Filter(count => count == 0) |
||||
.Map(_ => channel) |
||||
.ToValidation<BaseError>("Channel already has one playout"); |
||||
|
||||
private Validation<BaseError, string> ValidateScheduleFile(CreateScriptedPlayout request) |
||||
{ |
||||
if (!_localFileSystem.FileExists(request.ScheduleFile)) |
||||
{ |
||||
return BaseError.New("Scripted schedule does not exist!"); |
||||
} |
||||
|
||||
return request.ScheduleFile; |
||||
} |
||||
|
||||
private static Validation<BaseError, PlayoutScheduleKind> ValidateScheduleKind( |
||||
CreateScriptedPlayout createScriptedPlayout) => |
||||
Optional(createScriptedPlayout.ScheduleKind) |
||||
.Filter(scheduleKind => scheduleKind == PlayoutScheduleKind.Scripted) |
||||
.ToValidation<BaseError>("[ScheduleKind] must be Scripted"); |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Scheduling; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Scheduling; |
||||
|
||||
public interface ISequentialPlayoutBuilder |
||||
{ |
||||
Task<PlayoutBuildResult> Build( |
||||
DateTimeOffset start, |
||||
Playout playout, |
||||
PlayoutReferenceData referenceData, |
||||
PlayoutBuildMode mode, |
||||
CancellationToken cancellationToken); |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
using System.Diagnostics.CodeAnalysis; |
||||
using ErsatzTV.Core.Interfaces.Scheduling; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules; |
||||
|
||||
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")] |
||||
public class ContentModule |
||||
{ |
||||
private Dictionary<string, IMediaCollectionEnumerator> _contentEnumerators = []; |
||||
|
||||
public bool add_collection(string key, string name, string order) |
||||
{ |
||||
if (_contentEnumerators.ContainsKey(key)) |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
Console.WriteLine($"Adding collection '{name}' with key '{key}' and order '{order}'"); |
||||
_contentEnumerators.Clear(); |
||||
|
||||
return true; |
||||
} |
||||
} |
||||
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Interfaces.Scheduling; |
||||
using ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules; |
||||
using IronPython.Hosting; |
||||
using IronPython.Runtime; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.ScriptedScheduling; |
||||
|
||||
public class ScriptedPlayoutBuilder( |
||||
//IConfigElementRepository configElementRepository,
|
||||
//IMediaCollectionRepository mediaCollectionRepository,
|
||||
ILocalFileSystem localFileSystem, |
||||
ILogger<ScriptedPlayoutBuilder> logger) |
||||
: IScriptedPlayoutBuilder |
||||
{ |
||||
public async Task<PlayoutBuildResult> Build( |
||||
DateTimeOffset start, |
||||
Playout playout, |
||||
PlayoutReferenceData referenceData, |
||||
PlayoutBuildMode mode, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await Task.Delay(10, cancellationToken); |
||||
|
||||
var result = PlayoutBuildResult.Empty; |
||||
|
||||
if (!localFileSystem.FileExists(playout.ScheduleFile)) |
||||
{ |
||||
logger.LogError("Cannot build scripted playout; schedule file {File} does not exist", playout.ScheduleFile); |
||||
return result; |
||||
} |
||||
|
||||
logger.LogInformation("Building scripted playout..."); |
||||
|
||||
//int daysToBuild = await GetDaysToBuild();
|
||||
//DateTimeOffset finish = start.AddDays(daysToBuild);
|
||||
|
||||
//var enumeratorCache = new EnumeratorCache(mediaCollectionRepository, logger);
|
||||
|
||||
// apply all history???
|
||||
|
||||
try |
||||
{ |
||||
var engine = Python.CreateEngine(); |
||||
var scope = engine.CreateScope(); |
||||
|
||||
var sys = engine.GetSysModule(); |
||||
var modules = (PythonDictionary)sys.GetVariable("modules"); |
||||
|
||||
var types = engine.ImportModule("types"); |
||||
dynamic moduleType = types.GetVariable("ModuleType"); |
||||
|
||||
dynamic ersatztv = engine.Operations.Invoke(moduleType, "ersatztv"); |
||||
modules["ersatztv"] = ersatztv; |
||||
|
||||
var contentModule = new ContentModule(); |
||||
engine.Operations.SetMember(ersatztv, "content", contentModule); |
||||
|
||||
engine.ExecuteFile(playout.ScheduleFile, scope); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
logger.LogError(ex, "Unexpected error building scripted playout"); |
||||
throw; |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
// private async Task<int> GetDaysToBuild() =>
|
||||
// await configElementRepository
|
||||
// .GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
|
||||
// .IfNoneAsync(2);
|
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_PlayoutScheduleFile : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<string>( |
||||
name: "ScheduleFile", |
||||
table: "Playout", |
||||
type: "longtext", |
||||
nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.Sql("UPDATE `Playout` SET `ScheduleFile` = COALESCE(`ExternalJsonFile`, `TemplateFile`)"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "ScheduleFile", |
||||
table: "Playout"); |
||||
} |
||||
} |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Remove_PlayoutExternalJsonFileTemplateFile : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "ExternalJsonFile", |
||||
table: "Playout"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "TemplateFile", |
||||
table: "Playout"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<string>( |
||||
name: "ExternalJsonFile", |
||||
table: "Playout", |
||||
type: "longtext", |
||||
nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "TemplateFile", |
||||
table: "Playout", |
||||
type: "longtext", |
||||
nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
} |
||||
} |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_PlayoutScheduleFile : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<string>( |
||||
name: "ScheduleFile", |
||||
table: "Playout", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.Sql("UPDATE `Playout` SET `ScheduleFile` = COALESCE(`ExternalJsonFile`, `TemplateFile`)"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "ScheduleFile", |
||||
table: "Playout"); |
||||
} |
||||
} |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Remove_PlayoutExternalJsonFileTemplateFile : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "ExternalJsonFile", |
||||
table: "Playout"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "TemplateFile", |
||||
table: "Playout"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<string>( |
||||
name: "ExternalJsonFile", |
||||
table: "Playout", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "TemplateFile", |
||||
table: "Playout", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue