diff --git a/CHANGELOG.md b/CHANGELOG.md index e4b4828f5..cff333a93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - `Normalize Video` (default: true) - normalizes video streams, or stream copies when disabled - `Normalize Colors` (default: true) - normalizes color parameters when enabled - Disabling any of these options may have a significant performance benefit *at the expense of stream stability* +- Add chapter `title` to filler expression + - This can be used to include or exclude chapters with specific (case-insensitive) titles + - E.g. `title == 'here'`, `title != 'not here'`, `title like '%here%'` ### Changed - Disable automatic artwork database cleanup diff --git a/ErsatzTV.Core.Tests/Scheduling/FillerExpressionTests.cs b/ErsatzTV.Core.Tests/Scheduling/FillerExpressionTests.cs index 4f8419dfe..9c816a7d8 100644 --- a/ErsatzTV.Core.Tests/Scheduling/FillerExpressionTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/FillerExpressionTests.cs @@ -75,4 +75,106 @@ public class FillerExpressionTests result[1].EndTime.ShouldBe(TimeSpan.FromMinutes(20)); result[2].EndTime.ShouldBe(TimeSpan.FromMinutes(30)); } + + [Test] + public void Match_Case_Insensitive_Titles_Expression() + { + // 30 min content + var playoutItem = new PlayoutItem { Start = DateTimeOffset.Now.UtcDateTime }; + playoutItem.Finish = playoutItem.Start + TimeSpan.FromMinutes(30); + + // chapters every 5 min + var chapters = new List + { + new() { ChapterId = 1, StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(5), Title = "Not Here" }, + new() { ChapterId = 2, StartTime = TimeSpan.FromMinutes(5), EndTime = TimeSpan.FromMinutes(10), Title = "Here" }, + new() { ChapterId = 3, StartTime = TimeSpan.FromMinutes(10), EndTime = TimeSpan.FromMinutes(15), Title = "Not Here" }, + new() { ChapterId = 4, StartTime = TimeSpan.FromMinutes(15), EndTime = TimeSpan.FromMinutes(20), Title = "Here" }, + new() { ChapterId = 5, StartTime = TimeSpan.FromMinutes(20), EndTime = TimeSpan.FromMinutes(25), Title = "Not Here" }, + new() { ChapterId = 6, StartTime = TimeSpan.FromMinutes(25), EndTime = TimeSpan.FromMinutes(30), Title = "Here" } + }; + + var fillerPreset = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + Expression = + "title == 'here'" + }; + + List result = FillerExpression.FilterChapters(fillerPreset.Expression, chapters, playoutItem); + + result.Count.ShouldBe(3); + result[0].EndTime.ShouldBe(TimeSpan.FromMinutes(10)); + result[1].EndTime.ShouldBe(TimeSpan.FromMinutes(20)); + result[2].EndTime.ShouldBe(TimeSpan.FromMinutes(30)); + } + + [Test] + public void Exclude_Case_Insensitive_Titles_Expression() + { + // 30 min content + var playoutItem = new PlayoutItem { Start = DateTimeOffset.Now.UtcDateTime }; + playoutItem.Finish = playoutItem.Start + TimeSpan.FromMinutes(30); + + // chapters every 5 min + var chapters = new List + { + new() { ChapterId = 1, StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(5), Title = "Not Here" }, + new() { ChapterId = 2, StartTime = TimeSpan.FromMinutes(5), EndTime = TimeSpan.FromMinutes(10), Title = "Here" }, + new() { ChapterId = 3, StartTime = TimeSpan.FromMinutes(10), EndTime = TimeSpan.FromMinutes(15), Title = "Not Here" }, + new() { ChapterId = 4, StartTime = TimeSpan.FromMinutes(15), EndTime = TimeSpan.FromMinutes(20), Title = "Here" }, + new() { ChapterId = 5, StartTime = TimeSpan.FromMinutes(20), EndTime = TimeSpan.FromMinutes(25), Title = "Not Here" }, + new() { ChapterId = 6, StartTime = TimeSpan.FromMinutes(25), EndTime = TimeSpan.FromMinutes(30), Title = "Here" } + }; + + var fillerPreset = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + Expression = + "title != 'not here'" + }; + + List result = FillerExpression.FilterChapters(fillerPreset.Expression, chapters, playoutItem); + + result.Count.ShouldBe(3); + result[0].EndTime.ShouldBe(TimeSpan.FromMinutes(10)); + result[1].EndTime.ShouldBe(TimeSpan.FromMinutes(20)); + result[2].EndTime.ShouldBe(TimeSpan.FromMinutes(30)); + } + + [Test] + public void Include_Partial_Case_Insensitive_Titles_Expression() + { + // 30 min content + var playoutItem = new PlayoutItem { Start = DateTimeOffset.Now.UtcDateTime }; + playoutItem.Finish = playoutItem.Start + TimeSpan.FromMinutes(30); + + // chapters every 5 min + var chapters = new List + { + new() { ChapterId = 1, StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(5), Title = "Not Here" }, + new() { ChapterId = 2, StartTime = TimeSpan.FromMinutes(5), EndTime = TimeSpan.FromMinutes(10), Title = "Here" }, + new() { ChapterId = 3, StartTime = TimeSpan.FromMinutes(10), EndTime = TimeSpan.FromMinutes(15), Title = "Not Here" }, + new() { ChapterId = 4, StartTime = TimeSpan.FromMinutes(15), EndTime = TimeSpan.FromMinutes(20), Title = "Here" }, + new() { ChapterId = 5, StartTime = TimeSpan.FromMinutes(20), EndTime = TimeSpan.FromMinutes(25), Title = "Not Here" }, + new() { ChapterId = 6, StartTime = TimeSpan.FromMinutes(25), EndTime = TimeSpan.FromMinutes(30), Title = "Here" } + }; + + var fillerPreset = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + Expression = + "title like \"%here%\"" + }; + + List result = FillerExpression.FilterChapters(fillerPreset.Expression, chapters, playoutItem); + + result.Count.ShouldBe(6); + result[0].EndTime.ShouldBe(TimeSpan.FromMinutes(5)); + result[1].EndTime.ShouldBe(TimeSpan.FromMinutes(10)); + result[2].EndTime.ShouldBe(TimeSpan.FromMinutes(15)); + result[3].EndTime.ShouldBe(TimeSpan.FromMinutes(20)); + result[4].EndTime.ShouldBe(TimeSpan.FromMinutes(25)); + result[5].EndTime.ShouldBe(TimeSpan.FromMinutes(30)); + } } diff --git a/ErsatzTV.Core/Scheduling/FillerExpression.cs b/ErsatzTV.Core/Scheduling/FillerExpression.cs index afa3ee1ed..b3e634d90 100644 --- a/ErsatzTV.Core/Scheduling/FillerExpression.cs +++ b/ErsatzTV.Core/Scheduling/FillerExpression.cs @@ -15,7 +15,7 @@ public static class FillerExpression return effectiveChapters; } - var chapterPoints = effectiveChapters.Map(c => c.EndTime).SkipLast().ToList(); + var candidateChapters = effectiveChapters.SkipLast().ToList(); var newChapters = new List(); @@ -26,10 +26,11 @@ public static class FillerExpression double contentDuration = (playoutItem.FinishOffset - playoutItem.StartOffset).TotalSeconds; var matches = 0; - for (var index = 0; index < chapterPoints.Count; index++) + for (var index = 0; index < candidateChapters.Count; index++) { - TimeSpan chapterPoint = chapterPoints[index]; - var expression = new Expression(fillerExpression); + MediaChapter chapter = candidateChapters[index]; + TimeSpan chapterPoint = chapter.EndTime; + var expression = new Expression(fillerExpression, ExpressionOptions.CaseInsensitiveStringComparer); int chapterNum = index + 1; double sinceLastFiller = chapterPoint.TotalSeconds - lastFiller; int matchedPoints = matches; @@ -45,6 +46,7 @@ public static class FillerExpression "matched_points" => matchedPoints, "point" => chapterPoint.TotalSeconds, "num" => chapterNum, + "title" => chapter.Title ?? string.Empty, _ => e.Result }; }; diff --git a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs index 9176c095f..42a6de4da 100644 --- a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs @@ -692,7 +692,8 @@ public class TranscodingTests AudioFormat = FFmpegProfileAudioFormat.Aac, DeinterlaceVideo = true, BitDepth = profileBitDepth, - ScalingBehavior = scalingBehavior + ScalingBehavior = scalingBehavior, + NormalizeColors = profileNormalizeColors }, StreamingMode = streamingMode, SubtitleMode = subtitleMode