From 25f4fb22e550f5a251a09db09bca78fd75d19307 Mon Sep 17 00:00:00 2001
From: Jason Dove <1695733+jasongdove@users.noreply.github.com>
Date: Sat, 21 Jun 2025 14:28:41 -0500
Subject: [PATCH] yaml sequence improvements (#2053)

---
 CHANGELOG.md                                  |  5 +++
 .../Models/YamlPlayoutSequenceInstruction.cs  |  1 +
 .../YamlScheduling/YamlPlayoutBuilder.cs      | 44 +++++++++++++------
 3 files changed, 37 insertions(+), 13 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5a820891..0c4f15ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Add health check error when invalid VAAPI device and VAAPI driver combination is used in an active ffmpeg profile
   - This makes it obvious when hardware acceleration will not work as configured
 - Add button in schedule editor to clone schedule item
+- Allow YAML playout sequence definitions to reference other sequences
+  - Playout builder will behave in unexpected ways if nesting is too deep
+- Add `repeat` property to YAML sequence instruction
+  - This tells the playout builder how many times this sequence should repeat
+  - Omitting this value is the same as setting it to `1`
 
 ### Changed
 - Start to make UI minimally responsive (functional on smaller screens)
diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSequenceInstruction.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSequenceInstruction.cs
index 98107505..4d7871a8 100644
--- a/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSequenceInstruction.cs
+++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSequenceInstruction.cs
@@ -3,4 +3,5 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
 public class YamlPlayoutSequenceInstruction : YamlPlayoutInstruction
 {
     public string Sequence { get; set; }
+    public int Repeat { get; set; }
 }
diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs
index ca4efc1a..d07329b9 100644
--- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs
+++ b/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs
@@ -127,7 +127,19 @@ public class YamlPlayoutBuilder(
             }
         }
 
-        FlattenSequences(context);
+        int flattenCount = 0;
+        while (context.Definition.Playout.Any(x => x is YamlPlayoutSequenceInstruction))
+        {
+            if (flattenCount > 10)
+            {
+                logger.LogError(
+                    "YAML playout definition contains sequence nesting that is too deep; this introduces undefined behavior");
+                break;
+            }
+
+            FlattenSequences(context);
+            flattenCount++;
+        }
 
         // handle all playout instructions
         while (context.CurrentTime < finish)
@@ -204,25 +216,31 @@ public class YamlPlayoutBuilder(
                         .Filter(s => s.Key == sequenceInstruction.Sequence)
                         .HeadOrNone()
                         .Map(s => s.Items)
-                        .Flatten();
+                        .Flatten()
+                        .ToList();
 
                     var sequenceGuid = Guid.NewGuid();
+                    int repeat = sequenceInstruction.Repeat > 0 ? sequenceInstruction.Repeat : 1;
 
-                    // insert all instructions from the sequence
-                    foreach (YamlPlayoutInstruction i in sequenceInstructions)
+                    for (var r = 0; r < repeat; r++)
                     {
-                        // used for shuffling
-                        i.SequenceKey = sequenceInstruction.Sequence;
-                        i.SequenceGuid = sequenceGuid;
-
-                        // copy custom title
-                        if (!string.IsNullOrWhiteSpace(sequenceInstruction.CustomTitle))
+                        // insert all instructions from the sequence
+                        foreach (YamlPlayoutInstruction i in sequenceInstructions)
                         {
-                            i.CustomTitle = sequenceInstruction.CustomTitle;
-                        }
+                            // used for shuffling
+                            i.SequenceKey = sequenceInstruction.Sequence;
+                            i.SequenceGuid = sequenceGuid;
+
+                            // copy custom title
+                            if (!string.IsNullOrWhiteSpace(sequenceInstruction.CustomTitle))
+                            {
+                                i.CustomTitle = sequenceInstruction.CustomTitle;
+                            }
 
-                        context.Definition.Playout.Add(i);
+                            context.Definition.Playout.Add(i);
+                        }
                     }
+
                     break;
                 default:
                     context.Definition.Playout.Add(instruction);