Browse Source

add custom stream selector content_condition (#2311)

pull/2312/head
Jason Dove 4 days ago committed by GitHub
parent
commit
4b18ee6b66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      CHANGELOG.md
  2. 216
      ErsatzTV.Core.Tests/FFmpeg/CustomStreamSelectorTests.cs
  3. 32
      ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs
  4. 1
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  5. 3
      ErsatzTV.Core/FFmpeg/Selector/StreamSelectorItem.cs
  6. 1
      ErsatzTV.Core/Interfaces/FFmpeg/ICustomStreamSelector.cs

5
CHANGELOG.md

@ -38,6 +38,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -38,6 +38,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- The `variables` property can be used to dynamically replace text from the template
- `graphics_off` will turn off a specific element, or all elements if none are specified
- Add `Seek Seconds` to playback troubleshooting to support capturing timing-related issues
- Custom stream selector: add `content_condition` to allow channel and time-of-day based decisions
- `content_condition` expression can use
- `channel_number`
- `channel_name`
- `time_of_day_seconds` - the start time for the current item, represented in seconds since midnight
### Fix
- Fix database operations that were slowing down playout builds

216
ErsatzTV.Core.Tests/FFmpeg/CustomStreamSelectorTests.cs

@ -46,7 +46,7 @@ public class CustomStreamSelectorTests @@ -46,7 +46,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_eng_Audio_Exact_Match()
{
const string YAML =
const string yaml =
"""
---
items:
@ -55,10 +55,10 @@ public class CustomStreamSelectorTests @@ -55,10 +55,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
@ -72,7 +72,7 @@ public class CustomStreamSelectorTests @@ -72,7 +72,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_und_Audio_Missing_Language()
{
const string YAML =
const string yaml =
"""
---
items:
@ -80,10 +80,10 @@ public class CustomStreamSelectorTests @@ -80,10 +80,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
@ -97,7 +97,7 @@ public class CustomStreamSelectorTests @@ -97,7 +97,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_eng_Audio_Exact_Match_Multiple_Audio_Languages()
{
const string YAML =
const string yaml =
"""
---
items:
@ -105,10 +105,10 @@ public class CustomStreamSelectorTests @@ -105,10 +105,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
@ -122,7 +122,7 @@ public class CustomStreamSelectorTests @@ -122,7 +122,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_eng_Audio_Exact_Match_Multiple_Items()
{
const string YAML =
const string yaml =
"""
---
items:
@ -136,10 +136,10 @@ public class CustomStreamSelectorTests @@ -136,10 +136,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
@ -153,7 +153,7 @@ public class CustomStreamSelectorTests @@ -153,7 +153,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_eng_Audio_Pattern_Match()
{
const string YAML =
const string yaml =
"""
---
items:
@ -162,10 +162,10 @@ public class CustomStreamSelectorTests @@ -162,10 +162,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
@ -179,7 +179,7 @@ public class CustomStreamSelectorTests @@ -179,7 +179,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_en_Audio_Pattern_Match()
{
const string YAML =
const string yaml =
"""
---
items:
@ -189,10 +189,10 @@ public class CustomStreamSelectorTests @@ -189,10 +189,10 @@ public class CustomStreamSelectorTests
_audioVersion = GetTestAudioVersion("en");
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
@ -206,7 +206,7 @@ public class CustomStreamSelectorTests @@ -206,7 +206,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task disable_subtitles_Should_Select_No_Subtitles()
{
const string YAML =
const string yaml =
"""
---
items:
@ -216,10 +216,10 @@ public class CustomStreamSelectorTests @@ -216,10 +216,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.Subtitle.IsSome.ShouldBeFalse();
}
@ -227,7 +227,7 @@ public class CustomStreamSelectorTests @@ -227,7 +227,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_eng_Subtitle_Exact_Match()
{
const string YAML =
const string yaml =
"""
---
items:
@ -238,10 +238,10 @@ public class CustomStreamSelectorTests @@ -238,10 +238,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.Subtitle.IsSome.ShouldBeTrue();
@ -255,7 +255,7 @@ public class CustomStreamSelectorTests @@ -255,7 +255,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_eng_Subtitle_Pattern_Match()
{
const string YAML =
const string yaml =
"""
---
items:
@ -266,10 +266,10 @@ public class CustomStreamSelectorTests @@ -266,10 +266,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.Subtitle.IsSome.ShouldBeTrue();
@ -283,7 +283,7 @@ public class CustomStreamSelectorTests @@ -283,7 +283,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_en_Subtitle_Pattern_Match()
{
const string YAML =
const string yaml =
"""
---
items:
@ -300,10 +300,10 @@ public class CustomStreamSelectorTests @@ -300,10 +300,10 @@ public class CustomStreamSelectorTests
];
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.Subtitle.IsSome.ShouldBeTrue();
@ -317,7 +317,7 @@ public class CustomStreamSelectorTests @@ -317,7 +317,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_No_Subtitle_Exact_Match_Multiple_Items()
{
const string YAML =
const string yaml =
"""
---
items:
@ -331,10 +331,10 @@ public class CustomStreamSelectorTests @@ -331,10 +331,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
@ -350,7 +350,7 @@ public class CustomStreamSelectorTests @@ -350,7 +350,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_Foreign_Audio_And_English_Subtitle_Multiple_Items()
{
const string YAML =
const string yaml =
"""
---
items:
@ -364,10 +364,88 @@ public class CustomStreamSelectorTests @@ -364,10 +364,88 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
foreach (MediaStream audioStream in result.AudioStream)
{
audioStream.Index.ShouldBe(0);
audioStream.Language.ShouldBe("ja");
}
result.Subtitle.IsSome.ShouldBeTrue();
foreach (Subtitle subtitle in result.Subtitle)
{
subtitle.Id.ShouldBe(1);
subtitle.Language.ShouldBe("eng");
}
}
[Test]
public async Task Should_Select_English_Audio_No_Subtitles_Time_Of_Day_Content_Condition_Fail()
{
const string yaml =
"""
---
items:
- audio_language: ["ja"]
subtitle_language: ["eng"]
content_condition: "time_of_day_seconds >= 43200"
- audio_language: ["eng"]
disable_subtitles: true
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now.LocalDateTime.Date.AddHours(11).AddMinutes(59), // 11:59 AM
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
foreach (MediaStream audioStream in result.AudioStream)
{
audioStream.Index.ShouldBe(1);
audioStream.Language.ShouldBe("eng");
}
result.Subtitle.IsSome.ShouldBeFalse();
}
[Test]
public async Task Should_Select_Foreign_Audio_And_English_Subtitle_Time_Of_Day_Content_Condition_Match()
{
const string yaml =
"""
---
items:
- audio_language: ["ja"]
subtitle_language: ["eng"]
content_condition: "time_of_day_seconds < 43200"
- audio_language: ["eng"]
disable_subtitles: true
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now.LocalDateTime.Date.AddHours(11).AddMinutes(59), // 11:59 AM
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
@ -389,7 +467,7 @@ public class CustomStreamSelectorTests @@ -389,7 +467,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Ignore_Blocked_Audio_Title()
{
const string YAML =
const string yaml =
"""
---
items:
@ -400,10 +478,10 @@ public class CustomStreamSelectorTests @@ -400,10 +478,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
@ -417,7 +495,7 @@ public class CustomStreamSelectorTests @@ -417,7 +495,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_Allowed_Audio_Title()
{
const string YAML =
const string yaml =
"""
---
items:
@ -428,10 +506,10 @@ public class CustomStreamSelectorTests @@ -428,10 +506,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
@ -445,7 +523,7 @@ public class CustomStreamSelectorTests @@ -445,7 +523,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Ignore_Blocked_Subtitle_Title()
{
const string YAML =
const string yaml =
"""
---
items:
@ -458,10 +536,10 @@ public class CustomStreamSelectorTests @@ -458,10 +536,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.Subtitle.IsSome.ShouldBeTrue();
@ -475,7 +553,7 @@ public class CustomStreamSelectorTests @@ -475,7 +553,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_Allowed_Subtitle_Title()
{
const string YAML =
const string yaml =
"""
---
items:
@ -488,10 +566,10 @@ public class CustomStreamSelectorTests @@ -488,10 +566,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.Subtitle.IsSome.ShouldBeTrue();
@ -505,7 +583,7 @@ public class CustomStreamSelectorTests @@ -505,7 +583,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_Condition_Forced_Subtitle()
{
const string YAML =
const string yaml =
"""
---
items:
@ -515,10 +593,10 @@ public class CustomStreamSelectorTests @@ -515,10 +593,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.Subtitle.IsSome.ShouldBeTrue();
@ -532,7 +610,7 @@ public class CustomStreamSelectorTests @@ -532,7 +610,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_Condition_External_Subtitle()
{
const string YAML =
const string yaml =
"""
---
items:
@ -542,10 +620,10 @@ public class CustomStreamSelectorTests @@ -542,10 +620,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.Subtitle.IsSome.ShouldBeTrue();
@ -559,7 +637,7 @@ public class CustomStreamSelectorTests @@ -559,7 +637,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_Condition_Audio_Title()
{
const string YAML =
const string yaml =
"""
---
items:
@ -569,10 +647,10 @@ public class CustomStreamSelectorTests @@ -569,10 +647,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
@ -586,7 +664,7 @@ public class CustomStreamSelectorTests @@ -586,7 +664,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_Condition_Audio_Channels()
{
const string YAML =
const string yaml =
"""
---
items:
@ -596,10 +674,10 @@ public class CustomStreamSelectorTests @@ -596,10 +674,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
@ -613,7 +691,7 @@ public class CustomStreamSelectorTests @@ -613,7 +691,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_Prioritized_Audio_Language()
{
const string YAML =
const string yaml =
"""
---
items:
@ -622,10 +700,10 @@ public class CustomStreamSelectorTests @@ -622,10 +700,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.AudioStream.IsSome.ShouldBeTrue();
@ -639,7 +717,7 @@ public class CustomStreamSelectorTests @@ -639,7 +717,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_Prioritized_Subtitle_Language()
{
const string YAML =
const string yaml =
"""
---
items:
@ -649,10 +727,10 @@ public class CustomStreamSelectorTests @@ -649,10 +727,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.Subtitle.IsSome.ShouldBeTrue();
@ -666,7 +744,7 @@ public class CustomStreamSelectorTests @@ -666,7 +744,7 @@ public class CustomStreamSelectorTests
[Test]
public async Task Should_Select_No_Streams_When_Languages_Do_Not_Match()
{
const string YAML =
const string yaml =
"""
---
items:
@ -677,10 +755,10 @@ public class CustomStreamSelectorTests @@ -677,10 +755,10 @@ public class CustomStreamSelectorTests
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles);
result.AudioStream.IsSome.ShouldBeFalse();
result.Subtitle.IsSome.ShouldBeFalse();

32
ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs

@ -15,6 +15,7 @@ public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<Cust @@ -15,6 +15,7 @@ public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<Cust
{
public async Task<StreamSelectorResult> SelectStreams(
Channel channel,
DateTimeOffset contentStartTime,
MediaItemAudioVersion audioVersion,
List<Subtitle> allSubtitles)
{
@ -38,6 +39,17 @@ public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<Cust @@ -38,6 +39,17 @@ public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<Cust
foreach (StreamSelectorItem streamSelectorItem in streamSelector.Items)
{
if (!string.IsNullOrWhiteSpace(streamSelectorItem.ContentCondition))
{
if (!ContentMatchesCondition(channel, contentStartTime, streamSelectorItem.ContentCondition))
{
logger.LogDebug(
"Content does not match selector item {@SelectorItem}",
streamSelectorItem);
continue;
}
}
var candidateAudioStreams = audioStreams.ToDictionary(a => a, _ => int.MaxValue);
var candidateSubtitles = allSubtitles.ToDictionary(s => s, _ => int.MaxValue);
@ -281,6 +293,26 @@ public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<Cust @@ -281,6 +293,26 @@ public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<Cust
return expression.Evaluate() as bool? == true;
}
private static bool ContentMatchesCondition(
Channel channel,
DateTimeOffset contentStartTime,
string contentCondition)
{
var expression = new Expression(contentCondition);
expression.EvaluateParameter += (name, e) =>
{
e.Result = name switch
{
"channel_number" => channel.Number,
"channel_name" => channel.Name,
"time_of_day_seconds" => contentStartTime.LocalDateTime.TimeOfDay.TotalSeconds,
_ => e.Result
};
};
return expression.Evaluate() as bool? == true;
}
private async Task<StreamSelector> LoadStreamSelector(string streamSelectorFile)
{
try

1
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -110,6 +110,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -110,6 +110,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
{
StreamSelectorResult result = await _customStreamSelector.SelectStreams(
channel,
start,
audioVersion,
allSubtitles);
maybeAudioStream = result.AudioStream;

3
ErsatzTV.Core/FFmpeg/Selector/StreamSelectorItem.cs

@ -30,4 +30,7 @@ public class StreamSelectorItem @@ -30,4 +30,7 @@ public class StreamSelectorItem
[YamlMember(Alias = "subtitle_condition", ApplyNamingConventions = false)]
public string SubtitleCondition { get; set; }
[YamlMember(Alias = "content_condition", ApplyNamingConventions = false)]
public string ContentCondition { get; set; }
}

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

@ -7,6 +7,7 @@ public interface ICustomStreamSelector @@ -7,6 +7,7 @@ public interface ICustomStreamSelector
{
Task<StreamSelectorResult> SelectStreams(
Channel channel,
DateTimeOffset contentStartTime,
MediaItemAudioVersion audioVersion,
List<Subtitle> allSubtitles);
}

Loading…
Cancel
Save