mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* start to reorganize scripted playout building * add openapi * add all content fns * add playout instructions * add control instructions * add request models * prevent build loop * rename * update changelog * tweak changelogpull/2368/head
32 changed files with 737 additions and 595 deletions
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record AddAllRequestModel |
||||
{ |
||||
public string Content { get; set; } |
||||
public string FillerKind { get; set; } |
||||
public string CustomTitle { get; set; } |
||||
public bool DisableWatermarks { get; set; } |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record AddCollectionRequestModel |
||||
{ |
||||
public string Key { get; init; } |
||||
public string Collection { get; init; } |
||||
public string Order { get; init; } = "shuffle"; |
||||
} |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record AddCountRequestModel |
||||
{ |
||||
public string Content { get; set; } |
||||
public int Count { get; set; } |
||||
public string FillerKind { get; set; } |
||||
public string CustomTitle { get; set; } |
||||
public bool DisableWatermarks { get; set; } |
||||
} |
||||
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record AddDurationRequestModel |
||||
{ |
||||
public string Content { get; set; } |
||||
public string Duration { get; set; } |
||||
public string Fallback { get; set; } |
||||
public bool Trim { get; set; } |
||||
public int DiscardAttempts { get; set; } |
||||
public bool StopBeforeEnd { get; set; } = true; |
||||
public bool OfflineTail { get; set; } |
||||
public string FillerKind { get; set; } |
||||
public string CustomTitle { get; set; } |
||||
public bool DisableWatermarks { get; set; } |
||||
} |
||||
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record AddMarathonRequestModel |
||||
{ |
||||
public string Key { get; set; } |
||||
public string GroupBy { get; set; } |
||||
public string ItemOrder { get; set; } = "shuffle"; |
||||
public Dictionary<string, List<string>> Guids { get; set; } = []; |
||||
public List<string> Searches { get; set; } = []; |
||||
public bool PlayAllItems { get; set; } |
||||
public bool ShuffleGroups { get; set; } |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record AddMultiCollectionRequestModel |
||||
{ |
||||
public string Key { get; set; } |
||||
public string MultiCollection { get; set; } |
||||
public string Order { get; set; } = "shuffle"; |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record AddPlaylistRequestModel |
||||
{ |
||||
public string Key { get; set; } |
||||
public string Playlist { get; set; } |
||||
public string PlaylistGroup { get; set; } |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record AddShowRequestModel |
||||
{ |
||||
public string Key { get; set; } |
||||
public Dictionary<string, string> Guids { get; set; } = []; |
||||
public string Order { get; set; } = "shuffle"; |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record AddSmartCollectionRequestModel |
||||
{ |
||||
public string Key { get; set; } |
||||
public string SmartCollection { get; set; } |
||||
public string Order { get; set; } = "shuffle"; |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record ContextResponseModel( |
||||
DateTimeOffset CurrentTime, |
||||
DateTimeOffset StartTime, |
||||
DateTimeOffset FinishTime, |
||||
bool IsDone); |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record GraphicsOffRequestModel |
||||
{ |
||||
public List<string> Graphics { get; set; } = []; |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record GraphicsOnRequestModel |
||||
{ |
||||
public List<string> Graphics { get; set; } |
||||
public Dictionary<string, string> Variables { get; set; } = []; |
||||
} |
||||
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record PadToNextRequestModel |
||||
{ |
||||
public string Content { get; set; } |
||||
public int Minutes { get; set; } |
||||
public string Fallback { get; set; } |
||||
public bool Trim { get; set; } |
||||
public int DiscardAttempts { get; set; } |
||||
public bool StopBeforeEnd { get; set; } = true; |
||||
public bool OfflineTail { get; set; } = true; |
||||
public string FillerKind { get; set; } |
||||
public string CustomTitle { get; set; } |
||||
public bool DisableWatermarks { get; set; } |
||||
} |
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record PadUntilRequestModel |
||||
{ |
||||
public string Content { get; set; } |
||||
public string When { get; set; } |
||||
public bool Tomorrow { get; set; } |
||||
public string Fallback { get; set; } |
||||
public bool Trim { get; set; } |
||||
public int DiscardAttempts { get; set; } |
||||
public bool StopBeforeEnd { get; set; } = true; |
||||
public bool OfflineTail { get; set; } |
||||
public string FillerKind { get; set; } |
||||
public string CustomTitle { get; set; } |
||||
public bool DisableWatermarks { get; set; } |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record SkipItemsRequestModel |
||||
{ |
||||
public string Content { get; set; } |
||||
public int Count { get; set; } |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record SkipToItemRequestModel |
||||
{ |
||||
public string Content { get; set; } |
||||
public int Season { get; set; } |
||||
public int Episode { get; set; } |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record StartEpgGroupRequestModel |
||||
{ |
||||
public bool Advance { get; set; } = true; |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record WaitUntilRequestModel |
||||
{ |
||||
public string When { get; set; } |
||||
public bool Tomorrow { get; set; } |
||||
public bool RewindOnReset { get; set; } |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record WatermarkOffRequestModel |
||||
{ |
||||
public List<string> Watermark { get; set; } = []; |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Api.ScriptedPlayout; |
||||
|
||||
public record WatermarkOnRequestModel |
||||
{ |
||||
public List<string> Watermark { get; set; } |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
using ErsatzTV.Core.Scheduling.Engine; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Scheduling; |
||||
|
||||
public interface IScriptedPlayoutBuilderService |
||||
{ |
||||
bool MockSession(ISchedulingEngine schedulingEngine, Guid buildId); |
||||
Guid StartSession(ISchedulingEngine schedulingEngine); |
||||
ISchedulingEngine GetEngine(Guid buildId); |
||||
void EndSession(Guid buildId); |
||||
} |
||||
@ -1,123 +0,0 @@
@@ -1,123 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Scheduling.Engine; |
||||
using IronPython.Runtime; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules; |
||||
|
||||
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")] |
||||
[SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits")] |
||||
[SuppressMessage("ReSharper", "InconsistentNaming")] |
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")] |
||||
public class ContentModule(ISchedulingEngine schedulingEngine, CancellationToken cancellationToken) |
||||
{ |
||||
public void add_search(string key, string query, string order) |
||||
{ |
||||
if (!Enum.TryParse(order, ignoreCase: true, out PlaybackOrder playbackOrder)) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
schedulingEngine.AddSearch(key, query, playbackOrder, cancellationToken).GetAwaiter().GetResult(); |
||||
} |
||||
|
||||
public void add_collection(string key, string collection, string order) |
||||
{ |
||||
if (!Enum.TryParse(order, ignoreCase: true, out PlaybackOrder playbackOrder)) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
schedulingEngine.AddCollection(key, collection, playbackOrder, cancellationToken).GetAwaiter().GetResult(); |
||||
} |
||||
|
||||
public void add_marathon( |
||||
string key, |
||||
string group_by, |
||||
string item_order = "shuffle", |
||||
PythonDictionary guids = null, |
||||
PythonList searches = null, |
||||
bool play_all_items = false, |
||||
bool shuffle_groups = false) |
||||
{ |
||||
|
||||
if (!Enum.TryParse(item_order, ignoreCase: true, out PlaybackOrder itemPlaybackOrder)) |
||||
{ |
||||
itemPlaybackOrder = PlaybackOrder.Shuffle; |
||||
} |
||||
|
||||
var mappedGuids = new Dictionary<string, List<string>>(); |
||||
if (guids != null) |
||||
{ |
||||
foreach (KeyValuePair<object, object> guid in guids) |
||||
{ |
||||
var guidKey = guid.Key.ToString(); |
||||
if (guidKey is not null && guid.Value is PythonList guidValues) |
||||
{ |
||||
mappedGuids.Add(guidKey, guidValues.Select(x => x.ToString()).ToList()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
var mappedSearches = new List<string>(); |
||||
if (searches != null) |
||||
{ |
||||
mappedSearches.AddRange(searches.Select(x => x.ToString())); |
||||
} |
||||
|
||||
// guids OR searches are required
|
||||
if (mappedGuids.Count == 0 && mappedSearches.Count == 0) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
schedulingEngine |
||||
.AddMarathon(key, mappedGuids, mappedSearches, group_by, shuffle_groups, itemPlaybackOrder, play_all_items) |
||||
.GetAwaiter() |
||||
.GetResult(); |
||||
} |
||||
|
||||
public void add_multi_collection(string key, string multi_collection, string order) |
||||
{ |
||||
if (!Enum.TryParse(order, ignoreCase: true, out PlaybackOrder playbackOrder)) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
schedulingEngine |
||||
.AddMultiCollection(key, multi_collection, playbackOrder, cancellationToken) |
||||
.GetAwaiter() |
||||
.GetResult(); |
||||
} |
||||
|
||||
public void add_playlist(string key, string playlist, string playlist_group) |
||||
{ |
||||
schedulingEngine.AddPlaylist(key, playlist, playlist_group, cancellationToken).GetAwaiter().GetResult(); |
||||
} |
||||
|
||||
public void add_smart_collection(string key, string smart_collection, string order) |
||||
{ |
||||
if (!Enum.TryParse(order, ignoreCase: true, out PlaybackOrder playbackOrder)) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
schedulingEngine |
||||
.AddSmartCollection(key, smart_collection, playbackOrder, cancellationToken) |
||||
.GetAwaiter() |
||||
.GetResult(); |
||||
} |
||||
|
||||
public void add_show(string key, PythonDictionary guids, string order) |
||||
{ |
||||
if (!Enum.TryParse(order, ignoreCase: true, out PlaybackOrder playbackOrder)) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
schedulingEngine |
||||
.AddShow(key, guids.ToDictionary(k => k.Key.ToString(), k => k.Value.ToString()), playbackOrder) |
||||
.GetAwaiter() |
||||
.GetResult(); |
||||
} |
||||
} |
||||
@ -1,298 +0,0 @@
@@ -1,298 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis; |
||||
using ErsatzTV.Core.Domain.Filler; |
||||
using ErsatzTV.Core.Scheduling.Engine; |
||||
using IronPython.Runtime; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules; |
||||
|
||||
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")] |
||||
[SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits")] |
||||
[SuppressMessage("ReSharper", "InconsistentNaming")] |
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")] |
||||
public class PlayoutModule(ISchedulingEngine schedulingEngine, CancellationToken cancellationToken) |
||||
{ |
||||
public int FailureCount { get; private set; } |
||||
|
||||
// content instructions
|
||||
|
||||
public void add_all( |
||||
string content, |
||||
string filler_kind = null, |
||||
string custom_title = null, |
||||
bool disable_watermarks = false) |
||||
{ |
||||
Option<FillerKind> maybeFillerKind = Option<FillerKind>.None; |
||||
if (Enum.TryParse(filler_kind, ignoreCase: true, out FillerKind fillerKind)) |
||||
{ |
||||
maybeFillerKind = fillerKind; |
||||
} |
||||
|
||||
bool success = schedulingEngine.AddAll(content, maybeFillerKind, custom_title, disable_watermarks); |
||||
if (success) |
||||
{ |
||||
FailureCount = 0; |
||||
} |
||||
else |
||||
{ |
||||
FailureCount++; |
||||
} |
||||
} |
||||
|
||||
public void add_count( |
||||
string content, |
||||
int count, |
||||
string filler_kind = null, |
||||
string custom_title = null, |
||||
bool disable_watermarks = false) |
||||
{ |
||||
Option<FillerKind> maybeFillerKind = Option<FillerKind>.None; |
||||
if (Enum.TryParse(filler_kind, ignoreCase: true, out FillerKind fillerKind)) |
||||
{ |
||||
maybeFillerKind = fillerKind; |
||||
} |
||||
|
||||
bool success = schedulingEngine.AddCount(content, count, maybeFillerKind, custom_title, disable_watermarks); |
||||
if (success) |
||||
{ |
||||
FailureCount = 0; |
||||
} |
||||
else |
||||
{ |
||||
FailureCount++; |
||||
} |
||||
} |
||||
|
||||
public void add_duration( |
||||
string content, |
||||
string duration, |
||||
string fallback = null, |
||||
bool trim = false, |
||||
int discard_attempts = 0, |
||||
bool stop_before_end = true, |
||||
bool offline_tail = false, |
||||
string filler_kind = null, |
||||
string custom_title = null, |
||||
bool disable_watermarks = false) |
||||
{ |
||||
Option<FillerKind> maybeFillerKind = Option<FillerKind>.None; |
||||
if (Enum.TryParse(filler_kind, ignoreCase: true, out FillerKind fillerKind)) |
||||
{ |
||||
maybeFillerKind = fillerKind; |
||||
} |
||||
|
||||
bool success = schedulingEngine.AddDuration( |
||||
content, |
||||
duration, |
||||
fallback, |
||||
trim, |
||||
discard_attempts, |
||||
stop_before_end, |
||||
offline_tail, |
||||
maybeFillerKind, |
||||
custom_title, |
||||
disable_watermarks); |
||||
|
||||
if (success) |
||||
{ |
||||
FailureCount = 0; |
||||
} |
||||
else |
||||
{ |
||||
FailureCount++; |
||||
} |
||||
} |
||||
|
||||
public void pad_to_next( |
||||
string content, |
||||
int minutes, |
||||
string fallback = null, |
||||
bool trim = false, |
||||
int discard_attempts = 0, |
||||
bool stop_before_end = true, |
||||
bool offline_tail = true, |
||||
string filler_kind = null, |
||||
string custom_title = null, |
||||
bool disable_watermarks = false) |
||||
{ |
||||
Option<FillerKind> maybeFillerKind = Option<FillerKind>.None; |
||||
if (Enum.TryParse(filler_kind, ignoreCase: true, out FillerKind fillerKind)) |
||||
{ |
||||
maybeFillerKind = fillerKind; |
||||
} |
||||
|
||||
bool success = schedulingEngine.PadToNext( |
||||
content, |
||||
minutes, |
||||
fallback, |
||||
trim, |
||||
discard_attempts, |
||||
stop_before_end, |
||||
offline_tail, |
||||
maybeFillerKind, |
||||
custom_title, |
||||
disable_watermarks); |
||||
|
||||
if (success) |
||||
{ |
||||
FailureCount = 0; |
||||
} |
||||
else |
||||
{ |
||||
FailureCount++; |
||||
} |
||||
} |
||||
|
||||
public void pad_until( |
||||
string content, |
||||
string when, |
||||
bool tomorrow = false, |
||||
string fallback = null, |
||||
bool trim = false, |
||||
int discard_attempts = 0, |
||||
bool stop_before_end = true, |
||||
bool offline_tail = false, |
||||
string filler_kind = null, |
||||
string custom_title = null, |
||||
bool disable_watermarks = false) |
||||
{ |
||||
Option<FillerKind> maybeFillerKind = Option<FillerKind>.None; |
||||
if (Enum.TryParse(filler_kind, ignoreCase: true, out FillerKind fillerKind)) |
||||
{ |
||||
maybeFillerKind = fillerKind; |
||||
} |
||||
|
||||
bool success = schedulingEngine.PadUntil( |
||||
content, |
||||
when, |
||||
tomorrow, |
||||
fallback, |
||||
trim, |
||||
discard_attempts, |
||||
stop_before_end, |
||||
offline_tail, |
||||
maybeFillerKind, |
||||
custom_title, |
||||
disable_watermarks); |
||||
|
||||
if (success) |
||||
{ |
||||
FailureCount = 0; |
||||
} |
||||
else |
||||
{ |
||||
FailureCount++; |
||||
} |
||||
} |
||||
|
||||
|
||||
// control instructions
|
||||
|
||||
public void start_epg_group(bool advance = true) |
||||
{ |
||||
schedulingEngine.LockGuideGroup(advance); |
||||
} |
||||
|
||||
public void stop_epg_group() |
||||
{ |
||||
schedulingEngine.UnlockGuideGroup(); |
||||
} |
||||
|
||||
public void graphics_on(string graphics, PythonDictionary variables = null) |
||||
{ |
||||
var maybeVariables = new Dictionary<string, string>(); |
||||
if (variables != null) |
||||
{ |
||||
maybeVariables = variables.ToDictionary(v => v.Key.ToString(), v => v.Value.ToString()); |
||||
} |
||||
|
||||
schedulingEngine |
||||
.GraphicsOn([graphics], maybeVariables, cancellationToken) |
||||
.GetAwaiter() |
||||
.GetResult(); |
||||
} |
||||
|
||||
public void graphics_on(PythonList graphics, PythonDictionary variables = null) |
||||
{ |
||||
var maybeVariables = new Dictionary<string, string>(); |
||||
if (variables != null) |
||||
{ |
||||
maybeVariables = variables.ToDictionary(v => v.Key.ToString(), v => v.Value.ToString()); |
||||
} |
||||
|
||||
schedulingEngine |
||||
.GraphicsOn( |
||||
graphics.Select(g => g.ToString()).ToList(), |
||||
maybeVariables, |
||||
cancellationToken) |
||||
.GetAwaiter() |
||||
.GetResult(); |
||||
} |
||||
|
||||
public void graphics_off(string graphics = null) |
||||
{ |
||||
if (string.IsNullOrWhiteSpace(graphics)) |
||||
{ |
||||
schedulingEngine.GraphicsOff([], cancellationToken).GetAwaiter().GetResult(); |
||||
} |
||||
else |
||||
{ |
||||
schedulingEngine.GraphicsOff([graphics], cancellationToken).GetAwaiter().GetResult(); |
||||
} |
||||
} |
||||
|
||||
public void graphics_off(PythonList graphics) |
||||
{ |
||||
schedulingEngine |
||||
.GraphicsOff(graphics.Select(g => g.ToString()).ToList(), cancellationToken) |
||||
.GetAwaiter() |
||||
.GetResult(); |
||||
} |
||||
|
||||
public void watermark_on(string watermark) |
||||
{ |
||||
schedulingEngine.WatermarkOn([watermark]).GetAwaiter().GetResult(); |
||||
} |
||||
|
||||
public void watermark_on(PythonList watermark) |
||||
{ |
||||
schedulingEngine |
||||
.WatermarkOn(watermark.Select(g => g.ToString()).ToList()) |
||||
.GetAwaiter() |
||||
.GetResult(); |
||||
} |
||||
|
||||
public void watermark_off(string watermark = null) |
||||
{ |
||||
if (string.IsNullOrWhiteSpace(watermark)) |
||||
{ |
||||
schedulingEngine.WatermarkOff([]).GetAwaiter().GetResult(); |
||||
} |
||||
else |
||||
{ |
||||
schedulingEngine.WatermarkOff([watermark]).GetAwaiter().GetResult(); |
||||
} |
||||
} |
||||
|
||||
public void watermark_off(PythonList watermark) |
||||
{ |
||||
schedulingEngine.WatermarkOff(watermark.Select(g => g.ToString()).ToList()).GetAwaiter().GetResult(); |
||||
} |
||||
|
||||
public void skip_items(string content, int count) |
||||
{ |
||||
schedulingEngine.SkipItems(content, count); |
||||
} |
||||
|
||||
public void skip_to_item(string content, int season, int episode) |
||||
{ |
||||
schedulingEngine.SkipToItem(content, season, episode); |
||||
} |
||||
|
||||
public void wait_until(string when, bool tomorrow = false, bool rewind_on_reset = false) |
||||
{ |
||||
if (TimeOnly.TryParse(when, out TimeOnly waitUntil)) |
||||
{ |
||||
schedulingEngine.WaitUntil(waitUntil, tomorrow, rewind_on_reset); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Concurrent; |
||||
using ErsatzTV.Core.Interfaces.Scheduling; |
||||
using ErsatzTV.Core.Scheduling.Engine; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.ScriptedScheduling; |
||||
|
||||
public class ScriptedPlayoutBuilderService : IScriptedPlayoutBuilderService |
||||
{ |
||||
private readonly ConcurrentDictionary<Guid, ISchedulingEngine> _sessions = new(); |
||||
|
||||
public bool MockSession(ISchedulingEngine schedulingEngine, Guid buildId) => |
||||
_sessions.TryAdd(buildId, schedulingEngine); |
||||
|
||||
public Guid StartSession(ISchedulingEngine schedulingEngine) |
||||
{ |
||||
var buildId = Guid.NewGuid(); |
||||
_sessions[buildId] = schedulingEngine; |
||||
return buildId; |
||||
} |
||||
|
||||
public ISchedulingEngine GetEngine(Guid buildId) => _sessions.GetValueOrDefault(buildId); |
||||
|
||||
public void EndSession(Guid buildId) |
||||
{ |
||||
_sessions.TryRemove(buildId, out _); |
||||
} |
||||
} |
||||
@ -0,0 +1,439 @@
@@ -0,0 +1,439 @@
|
||||
using ErsatzTV.Core.Api.ScriptedPlayout; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Filler; |
||||
using ErsatzTV.Core.Interfaces.Scheduling; |
||||
using ErsatzTV.Core.Scheduling.Engine; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
|
||||
namespace ErsatzTV.Controllers.Api; |
||||
|
||||
[ApiController] |
||||
[EndpointGroupName("scripted-schedule")] |
||||
[Route("api/scripted/playout/build/{buildId:guid}")] |
||||
public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedPlayoutBuilderService) : ControllerBase |
||||
{ |
||||
[HttpGet("context", Name = "GetContext")] |
||||
public ActionResult<ContextResponseModel> GetContext([FromRoute] Guid buildId) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
try |
||||
{ |
||||
var state = engine.GetState(); |
||||
return Ok(new ContextResponseModel(state.CurrentTime, state.Start, state.Finish, state.IsDone)); |
||||
} |
||||
catch (InvalidOperationException ex) |
||||
{ |
||||
return BadRequest(ex.Message); |
||||
} |
||||
} |
||||
|
||||
[HttpPost("add_collection", Name = "AddCollection")] |
||||
public async Task<IActionResult> AddCollection( |
||||
[FromRoute] |
||||
Guid buildId, |
||||
[FromBody] |
||||
AddCollectionRequestModel request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
if (!Enum.TryParse(request.Order, ignoreCase: true, out PlaybackOrder playbackOrder)) |
||||
{ |
||||
return BadRequest("Invalid playback order."); |
||||
} |
||||
|
||||
await engine.AddCollection(request.Key, request.Collection, playbackOrder, cancellationToken); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpPost("add_marathon", Name = "AddMarathon")] |
||||
public async Task<IActionResult> AddMarathon([FromRoute] Guid buildId, [FromBody] AddMarathonRequestModel request) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
if (!Enum.TryParse(request.ItemOrder, ignoreCase: true, out PlaybackOrder itemPlaybackOrder)) |
||||
{ |
||||
return BadRequest("Invalid item playback order."); |
||||
} |
||||
|
||||
await engine.AddMarathon( |
||||
request.Key, |
||||
request.Guids ?? [], |
||||
request.Searches ?? [], |
||||
request.GroupBy, |
||||
request.ShuffleGroups, |
||||
itemPlaybackOrder, |
||||
request.PlayAllItems); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpPost("add_multi_collection", Name = "AddMultiCollection")] |
||||
public async Task<IActionResult> AddMultiCollection( |
||||
[FromRoute] |
||||
Guid buildId, |
||||
[FromBody] |
||||
AddMultiCollectionRequestModel request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
if (!Enum.TryParse(request.Order, ignoreCase: true, out PlaybackOrder playbackOrder)) |
||||
{ |
||||
return BadRequest("Invalid playback order."); |
||||
} |
||||
|
||||
await engine.AddMultiCollection(request.Key, request.MultiCollection, playbackOrder, cancellationToken); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpPost("add_playlist", Name = "AddPlaylist")] |
||||
public async Task<IActionResult> AddPlaylist( |
||||
[FromRoute] |
||||
Guid buildId, |
||||
[FromBody] |
||||
AddPlaylistRequestModel request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
await engine.AddPlaylist(request.Key, request.Playlist, request.PlaylistGroup, cancellationToken); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpPost("add_smart_collection", Name = "AddSmartCollection")] |
||||
public async Task<IActionResult> AddSmartCollection( |
||||
[FromRoute] |
||||
Guid buildId, |
||||
[FromBody] |
||||
AddSmartCollectionRequestModel request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
if (!Enum.TryParse(request.Order, ignoreCase: true, out PlaybackOrder playbackOrder)) |
||||
{ |
||||
return BadRequest("Invalid playback order."); |
||||
} |
||||
|
||||
await engine.AddSmartCollection(request.Key, request.SmartCollection, playbackOrder, cancellationToken); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpPost("add_show", Name = "AddShow")] |
||||
public async Task<IActionResult> AddShow( |
||||
[FromRoute] |
||||
Guid buildId, |
||||
[FromBody] |
||||
AddShowRequestModel request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
if (!Enum.TryParse(request.Order, ignoreCase: true, out PlaybackOrder playbackOrder)) |
||||
{ |
||||
return BadRequest("Invalid playback order."); |
||||
} |
||||
|
||||
await engine.AddShow(request.Key, request.Guids, playbackOrder); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpPost("add_all", Name = "AddAll")] |
||||
public IActionResult AddAll([FromRoute] Guid buildId, [FromBody] AddAllRequestModel request) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
Option<FillerKind> maybeFillerKind = Option<FillerKind>.None; |
||||
if (Enum.TryParse(request.FillerKind, ignoreCase: true, out FillerKind fk)) |
||||
{ |
||||
maybeFillerKind = fk; |
||||
} |
||||
|
||||
engine.AddAll(request.Content, maybeFillerKind, request.CustomTitle, request.DisableWatermarks); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpPost("add_count", Name = "AddCount")] |
||||
public IActionResult AddCount( |
||||
[FromRoute] |
||||
Guid buildId, |
||||
[FromBody] |
||||
AddCountRequestModel request) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
Option<FillerKind> maybeFillerKind = Option<FillerKind>.None; |
||||
if (Enum.TryParse(request.FillerKind, ignoreCase: true, out FillerKind fk)) |
||||
{ |
||||
maybeFillerKind = fk; |
||||
} |
||||
|
||||
engine.AddCount( |
||||
request.Content, |
||||
request.Count, |
||||
maybeFillerKind, |
||||
request.CustomTitle, |
||||
request.DisableWatermarks); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpPost("add_duration", Name = "AddDuration")] |
||||
public IActionResult AddDuration([FromRoute] Guid buildId, [FromBody] AddDurationRequestModel request) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
Option<FillerKind> maybeFillerKind = Option<FillerKind>.None; |
||||
if (Enum.TryParse(request.FillerKind, ignoreCase: true, out FillerKind fk)) |
||||
{ |
||||
maybeFillerKind = fk; |
||||
} |
||||
|
||||
engine.AddDuration( |
||||
request.Content, |
||||
request.Duration, |
||||
request.Fallback, |
||||
request.Trim, |
||||
request.DiscardAttempts, |
||||
request.StopBeforeEnd, |
||||
request.OfflineTail, |
||||
maybeFillerKind, |
||||
request.CustomTitle, |
||||
request.DisableWatermarks); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpPost("pad_to_next", Name = "PadToNext")] |
||||
public IActionResult PadToNext([FromRoute] Guid buildId, [FromBody] PadToNextRequestModel request) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
Option<FillerKind> maybeFillerKind = Option<FillerKind>.None; |
||||
if (Enum.TryParse(request.FillerKind, ignoreCase: true, out FillerKind fk)) |
||||
{ |
||||
maybeFillerKind = fk; |
||||
} |
||||
|
||||
engine.PadToNext( |
||||
request.Content, |
||||
request.Minutes, |
||||
request.Fallback, |
||||
request.Trim, |
||||
request.DiscardAttempts, |
||||
request.StopBeforeEnd, |
||||
request.OfflineTail, |
||||
maybeFillerKind, |
||||
request.CustomTitle, |
||||
request.DisableWatermarks); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpPost("pad_until", Name = "PadUntil")] |
||||
public IActionResult PadUntil([FromRoute] Guid buildId, [FromBody] PadUntilRequestModel request) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
Option<FillerKind> maybeFillerKind = Option<FillerKind>.None; |
||||
if (Enum.TryParse(request.FillerKind, ignoreCase: true, out FillerKind fk)) |
||||
{ |
||||
maybeFillerKind = fk; |
||||
} |
||||
|
||||
engine.PadUntil( |
||||
request.Content, |
||||
request.When, |
||||
request.Tomorrow, |
||||
request.Fallback, |
||||
request.Trim, |
||||
request.DiscardAttempts, |
||||
request.StopBeforeEnd, |
||||
request.OfflineTail, |
||||
maybeFillerKind, |
||||
request.CustomTitle, |
||||
request.DisableWatermarks); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpGet("start_epg_group", Name = "StartEpgGroup")] |
||||
public IActionResult StartEpgGroup([FromRoute] Guid buildId, [FromBody] StartEpgGroupRequestModel request) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
engine.LockGuideGroup(request.Advance); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpGet("stop_epg_group", Name = "StopEpgGroup")] |
||||
public IActionResult StopEpgGroup([FromRoute] Guid buildId) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
engine.UnlockGuideGroup(); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpGet("graphics_on", Name = "GraphicsOn")] |
||||
public async Task<IActionResult> GraphicsOn( |
||||
[FromRoute] |
||||
Guid buildId, |
||||
[FromBody] |
||||
GraphicsOnRequestModel request, |
||||
CancellationToken cancellationToken = default) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
await engine.GraphicsOn(request.Graphics, request.Variables, cancellationToken); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpGet("graphics_off", Name = "GraphicsOff")] |
||||
public async Task<IActionResult> GraphicsOff( |
||||
[FromRoute] |
||||
Guid buildId, |
||||
[FromBody] |
||||
GraphicsOffRequestModel request, |
||||
CancellationToken cancellationToken = default) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
await engine.GraphicsOff(request.Graphics, cancellationToken); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpGet("watermark_on", Name = "WatermarkOn")] |
||||
public async Task<IActionResult> WatermarkOn([FromRoute] Guid buildId, [FromBody] WatermarkOnRequestModel request) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
await engine.WatermarkOn(request.Watermark); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpGet("watermark_off", Name = "WatermarkOff")] |
||||
public async Task<IActionResult> WatermarkOff([FromRoute] Guid buildId, [FromBody] WatermarkOffRequestModel request) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
await engine.WatermarkOff(request.Watermark); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpGet("skip_items", Name = "SkipItems")] |
||||
public IActionResult SkipItems([FromRoute] Guid buildId, [FromBody] SkipItemsRequestModel request) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
engine.SkipItems(request.Content, request.Count); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpGet("skip_to_item", Name = "SkipToItem")] |
||||
public IActionResult SkipToItem([FromRoute] Guid buildId, [FromBody] SkipToItemRequestModel request) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
engine.SkipToItem(request.Content, request.Season, request.Episode); |
||||
return Ok(); |
||||
} |
||||
|
||||
[HttpGet("wait_until", Name = "WaitUntil")] |
||||
public IActionResult WaitUntil( |
||||
[FromRoute] |
||||
Guid buildId, |
||||
[FromBody] |
||||
WaitUntilRequestModel request) |
||||
{ |
||||
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId); |
||||
if (engine == null) |
||||
{ |
||||
return NotFound($"Active build engine not found for build {buildId}."); |
||||
} |
||||
|
||||
if (TimeOnly.TryParse(request.When, out TimeOnly waitUntil)) |
||||
{ |
||||
engine.WaitUntil(waitUntil, request.Tomorrow, request.RewindOnReset); |
||||
} |
||||
|
||||
return Ok(); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue