From 3d8225a8315b31cc9dd5e2f65ce70a43530577c9 Mon Sep 17 00:00:00 2001 From: scosman Date: Tue, 21 Mar 2023 19:17:55 -0400 Subject: [PATCH] Add command line actions via json config file --- actions/actions.go | 62 +++++++++++++++++++++++++++++++++ actions/actions_test.go | 30 ++++++++++++++++ main.go | 15 ++++++++ mdns/client.go | 3 -- test_files/valid_actions_1.json | 16 +++++++++ test_files/valid_actions_2.json | 16 +++++++++ 6 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 actions/actions.go create mode 100644 actions/actions_test.go create mode 100644 test_files/valid_actions_1.json create mode 100644 test_files/valid_actions_2.json diff --git a/actions/actions.go b/actions/actions.go new file mode 100644 index 0000000..7dd9a24 --- /dev/null +++ b/actions/actions.go @@ -0,0 +1,62 @@ +package actions + +import ( + "encoding/json" + "errors" + "io/ioutil" + "log" + "os/exec" +) + +type ActionName string + +type AirplayCommandLineAction struct { + DeviceName string `json:"device_name"` + Command string `json:"command"` + CommandArgs string `json:"command_args"` + ActionName ActionName `json:"action"` +} + +type AirplayMusicActionRunner struct { + Actions []*AirplayCommandLineAction `json:"actions"` +} + +const ( + ACTION_NAME_START_PLAYING ActionName = "start_playing" + ACTION_NAME_END_PLAYING ActionName = "end_playing" +) + +// func DefaultParams(service string) *QueryParam { +func NewAirplayMusicActionRunner(configFilePath string) (*AirplayMusicActionRunner, error) { + configBytes, err := ioutil.ReadFile(configFilePath) + if err != nil { + return nil, err + } + var parsedRunner AirplayMusicActionRunner + err = json.Unmarshal(configBytes, &parsedRunner) + if err != nil { + return nil, err + } + + for _, action := range parsedRunner.Actions { + if action.ActionName != ACTION_NAME_START_PLAYING && action.ActionName != ACTION_NAME_END_PLAYING { + return nil, errors.New("Invalid action name") + } + } + + return &parsedRunner, nil +} + +func (r *AirplayMusicActionRunner) RunActionForDeviceState(deviceName string, isPlaying bool) { + for _, action := range r.Actions { + if action.DeviceName == deviceName { + if (isPlaying && action.ActionName == ACTION_NAME_START_PLAYING) || (!isPlaying && action.ActionName == ACTION_NAME_END_PLAYING) { + log.Printf("Running command: %v %v", action.Command, action.CommandArgs) + cmd := exec.Command(action.Command, action.CommandArgs) + if err := cmd.Run(); err != nil { + log.Printf("Error running command: %v %v", action.Command, action.CommandArgs) + } + } + } + } +} diff --git a/actions/actions_test.go b/actions/actions_test.go new file mode 100644 index 0000000..6eb837c --- /dev/null +++ b/actions/actions_test.go @@ -0,0 +1,30 @@ +package actions + +import ( + "log" + "os" + "path/filepath" + "testing" +) + +func TestActionsParseValid(t *testing.T) { + workingDirectory, _ := os.Getwd() + testFile := filepath.Join(workingDirectory, "../test_files/valid_actions_1.json") + log.Printf("wd: %v", testFile) + + actions, err := NewAirplayMusicActionRunner(testFile) + if err != nil { + t.Fatal(err) + } + if len(actions.Actions) != 2 { + t.Fatal("parse error -- wrong count") + } + first := actions.Actions[0] + if first.DeviceName != "device1" || first.Command != "echo" || first.ActionName != "start_playing" { + t.Fatal("didn't parse first action") + } + second := actions.Actions[1] + if second.DeviceName != "Stereo" || second.Command != "echo" || second.ActionName != "end_playing" { + t.Fatal("didn't parse second action") + } +} diff --git a/main.go b/main.go index 91be1ce..5bc93c8 100644 --- a/main.go +++ b/main.go @@ -2,14 +2,28 @@ package main import ( "fmt" + "log" "math" + "os" + "github.com/scosman/airplay-music-watcher/actions" "github.com/scosman/airplay-music-watcher/mdns" ) const DeviceSupportsRelayBitmask = 0x800 func main() { + args := os.Args + if len(args) != 2 { + // first arg is program path, so 2 == 1... + log.Fatal("command requires exactly 1 arg -- the path to the json config file") + } + jsonFilePath := args[1] + actionRunner, err := actions.NewAirplayMusicActionRunner(jsonFilePath) + if err != nil { + log.Fatalf("Error parsing json config file: %v", err) + } + entriesCh := make(chan *mdns.AirplayFlagsEntry, 4) defer close(entriesCh) go func() { @@ -21,6 +35,7 @@ func main() { // https://github.com/openairplay/airplay-spec/blob/master/src/status_flags.md isPlaying := (DeviceSupportsRelayBitmask & entry.Flags) > 0 fmt.Printf("Airplay Device \"%s\" event, is playing: %t\n", entry.DeviceName, isPlaying) + actionRunner.RunActionForDeviceState(entry.DeviceName, isPlaying) } }() diff --git a/mdns/client.go b/mdns/client.go index 0e31126..ca8693d 100644 --- a/mdns/client.go +++ b/mdns/client.go @@ -329,7 +329,6 @@ func (c *client) query(params *QueryParam) error { inp.Info = strings.Join(rr.Txt, "|") inp.InfoFields = rr.Txt inp.hasTXT = true - //fmt.Printf("TXT Entry: %v\nExtra: %v\n", resp.Answer, resp.Extra) for _, answer := range resp.Answer { hostName := answer.Header().Name if strings.Contains(hostName, "._airplay._tcp.local.") { @@ -394,8 +393,6 @@ func (c *client) query(params *QueryParam) error { m.SetQuestion(inp.Name, dns.TypePTR) m.RecursionDesired = false - // TODO - //log.Print("skipping sending up entry") if err := c.sendQuery(m); err != nil { log.Printf("[ERR] mdns: Failed to query instance %s: %v", inp.Name, err) } diff --git a/test_files/valid_actions_1.json b/test_files/valid_actions_1.json new file mode 100644 index 0000000..31c92ae --- /dev/null +++ b/test_files/valid_actions_1.json @@ -0,0 +1,16 @@ +{ + "actions": [ + { + "device_name": "device1", + "action": "start_playing", + "command": "echo", + "command_args": "'value to echo device 1'" + }, + { + "device_name": "Stereo", + "action": "end_playing", + "command": "echo", + "command_args": "'value to echo for stereo'" + } + ] +} \ No newline at end of file diff --git a/test_files/valid_actions_2.json b/test_files/valid_actions_2.json new file mode 100644 index 0000000..4137477 --- /dev/null +++ b/test_files/valid_actions_2.json @@ -0,0 +1,16 @@ +{ + "actions": [ + { + "device_name": "Stereo", + "action": "start_playing", + "command": "echo", + "command_args": "'value to echo stereo start'" + }, + { + "device_name": "Stereo", + "action": "end_playing", + "command": "echo", + "command_args": "'value to echo for stereo end'" + } + ] +} \ No newline at end of file