Files
qrun/cmd/qrunproxy/show.go

196 lines
5.4 KiB
Go

package main
import "fmt"
type Show struct {
Tracks []*Track `json:"tracks"`
Blocks []*Block `json:"blocks"`
Triggers []*Trigger `json:"triggers"`
}
type Track struct {
ID string `json:"id"`
Name string `json:"name"`
}
type Block struct {
ID string `json:"id"`
Type string `json:"type"`
Track string `json:"track,omitempty"`
Name string `json:"name"`
Loop bool `json:"loop,omitempty"`
weight int
}
type Trigger struct {
Source TriggerSource `json:"source"`
Targets []TriggerTarget `json:"targets"`
}
func (t *Trigger) String() string {
s := fmt.Sprintf("%s/%s ->", t.Source.Block, t.Source.Signal)
for _, target := range t.Targets {
s += fmt.Sprintf(" %s/%s", target.Block, target.Hook)
}
return s
}
type TriggerSource struct {
Block string `json:"block"`
Signal string `json:"signal"`
}
type TriggerTarget struct {
Block string `json:"block"`
Hook string `json:"hook"`
}
func (block *Block) hasDefinedTiming() bool {
if block.Type == "cue" || block.Type == "delay" {
return true
}
if block.Type == "media" && !block.Loop {
return true
}
return false
}
func isValidEventForBlock(block *Block, event string) bool {
if block.Type == "cue" {
return event == "GO"
}
switch event {
case "START", "FADE_OUT", "END":
return true
default:
return false
}
}
func (show *Show) Validate() error {
if show == nil {
return fmt.Errorf("show is nil")
}
trackIDs := map[string]bool{}
for _, track := range show.Tracks {
if trackIDs[track.ID] {
return fmt.Errorf("duplicate track id %q", track.ID)
}
trackIDs[track.ID] = true
}
blocksByID := map[string]*Block{}
for _, block := range show.Blocks {
if blocksByID[block.ID] != nil {
return fmt.Errorf("duplicate block id %q", block.ID)
}
blocksByID[block.ID] = block
if block.Type == "cue" {
continue
}
if !trackIDs[block.Track] {
return fmt.Errorf("block %q uses unknown track %q", block.ID, block.Track)
}
}
type blockEvent struct {
block string
event string
}
hookTargeted := map[blockEvent]bool{}
startTargeted := map[string]bool{}
sourceUsed := map[blockEvent]bool{}
signalTargetedBy := map[blockEvent]*Trigger{}
for _, trigger := range show.Triggers {
for _, target := range trigger.Targets {
signalTargetedBy[blockEvent{target.Block, target.Hook}] = trigger
}
}
for _, trigger := range show.Triggers {
sourceBlock := blocksByID[trigger.Source.Block]
if sourceBlock == nil {
return fmt.Errorf("trigger source block %q not found", trigger.Source.Block)
}
targetedTracks := map[string]string{}
for _, target := range trigger.Targets {
targetBlock := blocksByID[target.Block]
if prev, ok := targetedTracks[targetBlock.Track]; ok {
return fmt.Errorf("trigger conflict: %s targets multiple blocks on track %q (%q and %q)",
trigger, targetBlock.Track, prev, target.Block)
}
targetedTracks[targetBlock.Track] = target.Block
}
if t, ok := signalTargetedBy[blockEvent{trigger.Source.Block, trigger.Source.Signal}]; ok {
sameTrackSingle := len(trigger.Targets) == 1 && blocksByID[trigger.Targets[0].Block].Track == sourceBlock.Track
if !sameTrackSingle {
return fmt.Errorf("trigger conflict: %s vs %s", t, trigger)
}
}
if !isValidEventForBlock(sourceBlock, trigger.Source.Signal) {
return fmt.Errorf("trigger source signal %q is invalid for block %q", trigger.Source.Signal, trigger.Source.Block)
}
src := blockEvent{trigger.Source.Block, trigger.Source.Signal}
if sourceUsed[src] {
return fmt.Errorf("duplicate trigger source: block %q signal %q", trigger.Source.Block, trigger.Source.Signal)
}
sourceUsed[src] = true
for _, target := range trigger.Targets {
targetBlock := blocksByID[target.Block]
if targetBlock == nil {
return fmt.Errorf("trigger target block %q not found", target.Block)
}
if !isValidEventForBlock(targetBlock, target.Hook) {
return fmt.Errorf("trigger target hook %q is invalid for block %q", target.Hook, target.Block)
}
hookTargeted[blockEvent{target.Block, target.Hook}] = true
if target.Hook == "START" {
startTargeted[target.Block] = true
}
}
}
for _, block := range show.Blocks {
if block.Type == "cue" {
continue
}
if !startTargeted[block.ID] {
return fmt.Errorf("block %q has no trigger for its START", block.ID)
}
if !block.hasDefinedTiming() && !hookTargeted[blockEvent{block.ID, "FADE_OUT"}] && !hookTargeted[blockEvent{block.ID, "END"}] {
return fmt.Errorf("block %q has no defined timing and nothing triggers its FADE_OUT or END", block.ID)
}
}
for _, trigger := range show.Triggers {
sourceBlock := blocksByID[trigger.Source.Block]
for _, target := range trigger.Targets {
targetBlock := blocksByID[target.Block]
if sourceBlock.Type != "cue" && targetBlock.Type != "cue" && sourceBlock.Track == targetBlock.Track && target.Hook == "START" && trigger.Source.Signal != "END" {
return fmt.Errorf("same-track START trigger from %q to %q must use END signal, not %s", sourceBlock.ID, targetBlock.ID, trigger.Source.Signal)
}
}
if sourceBlock.hasDefinedTiming() {
continue
}
signal := trigger.Source.Signal
if signal != "FADE_OUT" && signal != "END" {
continue
}
if signal == "END" && hookTargeted[blockEvent{sourceBlock.ID, "FADE_OUT"}] {
continue
}
if !hookTargeted[blockEvent{sourceBlock.ID, signal}] {
return fmt.Errorf("block %q has no defined timing and nothing triggers its %s, so its %s signal will never fire", sourceBlock.ID, signal, signal)
}
}
return nil
}