Browse Source

Added the basics for commands

Apollo 3 years ago
parent
commit
6a6c83e7a1
10 changed files with 604 additions and 3 deletions
  1. 4 0
      glom/cover
  2. 53 0
      glom/cover.out
  3. 155 0
      glom/glom.go
  4. 247 0
      glom/glom_test.go
  5. 6 0
      glom/go.mod
  6. 2 0
      glom/go.sum
  7. 8 0
      go.mod
  8. 4 0
      go.sum
  9. 30 0
      keys.go
  10. 95 3
      poster.go

+ 4 - 0
glom/cover

@@ -0,0 +1,4 @@
+#!/bin/bash
+
+go test -coverprofile=cover.out
+go tool cover -html=cover.out

+ 53 - 0
glom/cover.out

@@ -0,0 +1,53 @@
+mode: set
+red-green/glom/glom.go:13.71,15.32 2 1
+red-green/glom/glom.go:18.2,18.37 1 1
+red-green/glom/glom.go:22.2,24.22 3 1
+red-green/glom/glom.go:29.2,29.20 1 1
+red-green/glom/glom.go:15.32,17.3 1 1
+red-green/glom/glom.go:18.37,20.3 1 1
+red-green/glom/glom.go:24.22,27.3 1 1
+red-green/glom/glom.go:34.64,36.36 2 1
+red-green/glom/glom.go:42.2,42.70 1 1
+red-green/glom/glom.go:45.2,45.41 1 1
+red-green/glom/glom.go:49.2,52.30 3 1
+red-green/glom/glom.go:57.2,57.20 1 1
+red-green/glom/glom.go:36.36,37.22 1 1
+red-green/glom/glom.go:38.22,39.36 1 1
+red-green/glom/glom.go:42.70,44.3 1 1
+red-green/glom/glom.go:45.41,47.3 1 1
+red-green/glom/glom.go:52.30,55.3 1 1
+red-green/glom/glom.go:60.45,64.37 2 1
+red-green/glom/glom.go:79.2,79.15 1 1
+red-green/glom/glom.go:65.19,68.26 3 1
+red-green/glom/glom.go:71.36,73.43 2 1
+red-green/glom/glom.go:76.22,77.31 1 1
+red-green/glom/glom.go:68.26,70.4 1 1
+red-green/glom/glom.go:73.43,75.4 1 1
+red-green/glom/glom.go:82.52,83.31 1 1
+red-green/glom/glom.go:88.2,88.14 1 1
+red-green/glom/glom.go:83.31,84.20 1 1
+red-green/glom/glom.go:84.20,86.4 1 1
+red-green/glom/glom.go:91.79,92.47 1 1
+red-green/glom/glom.go:117.2,117.117 1 1
+red-green/glom/glom.go:92.47,94.47 1 1
+red-green/glom/glom.go:95.20,97.18 2 1
+red-green/glom/glom.go:100.4,100.25 1 1
+red-green/glom/glom.go:101.37,103.18 2 1
+red-green/glom/glom.go:112.23,114.54 2 1
+red-green/glom/glom.go:97.18,99.5 1 0
+red-green/glom/glom.go:103.18,105.19 2 1
+red-green/glom/glom.go:108.5,108.24 1 1
+red-green/glom/glom.go:105.19,107.6 1 0
+red-green/glom/glom.go:109.10,111.5 1 0
+red-green/glom/glom.go:120.48,122.31 2 1
+red-green/glom/glom.go:125.2,125.15 1 1
+red-green/glom/glom.go:122.31,124.3 1 1
+red-green/glom/glom.go:128.63,134.36 5 1
+red-green/glom/glom.go:154.2,154.23 1 1
+red-green/glom/glom.go:134.36,137.57 1 1
+red-green/glom/glom.go:137.57,139.4 1 1
+red-green/glom/glom.go:139.9,140.18 1 1
+red-green/glom/glom.go:140.18,142.19 2 1
+red-green/glom/glom.go:142.19,145.6 1 0
+red-green/glom/glom.go:145.11,148.6 2 1
+red-green/glom/glom.go:149.10,151.5 1 1

+ 155 - 0
glom/glom.go

@@ -0,0 +1,155 @@
+package glom
+
+import (
+	"fmt"
+	"reflect"
+	"strconv"
+	"strings"
+
+	"github.com/fatih/structs"
+)
+
+// Based on sliceToInterface
+func mapToInterface(data interface{}) (map[string]interface{}, error) {
+	mapV := reflect.ValueOf(data)
+	if mapV.Kind() != reflect.Map {
+		return nil, fmt.Errorf("Failed to convert %v, given %v type to map[string]interface{}", mapV, reflect.TypeOf(data))
+	}
+	if mapV.IsNil() || !mapV.IsValid() {
+		return nil, fmt.Errorf("Given nil or empty map!")
+	}
+
+	result := make(map[string]interface{})
+	keys := mapV.MapKeys()
+	for k := range keys {
+		//fmt.Printf("%d/%d = %v", k, len(keys), mapV.MapIndex(keys[k]))
+		result[keys[k].String()] = mapV.MapIndex(keys[k]).Interface()
+	}
+
+	return result, nil
+}
+
+// https://gist.github.com/heri16/077282d46ae95d48d430a90fb6accdff
+// I only need the length
+func sliceToInterface(data interface{}) ([]interface{}, error) {
+	sliceV := reflect.ValueOf(data)
+	if sliceV.Kind() == reflect.Slice { // Prevent us from converting an interface to interface
+		switch data.(type) {
+		case []interface{}:
+			return data.([]interface{}), nil
+		}
+	}
+	if sliceV.Kind() != reflect.Slice && sliceV.Kind() != reflect.Array {
+		return nil, fmt.Errorf("Failed to convert %v, given %v type to []interface{}", sliceV, reflect.TypeOf(data))
+	}
+	if sliceV.IsNil() || !sliceV.IsValid() {
+		return nil, fmt.Errorf("Given nil or empty slice!")
+	}
+
+	length := sliceV.Len()
+	result := make([]interface{}, length)
+
+	for i := 0; i < length; i++ {
+		//fmt.Printf("%d/%d = %v\r\n", i, length-1, sliceV.Index(i))
+		result[i] = sliceV.Index(i).Interface()
+	}
+
+	return result, nil
+}
+
+func GetPossible(data interface{}) []string {
+	var result []string
+	//fmt.Printf("%v (%v)\r\n", reflect.TypeOf(data).Kind(), reflect.TypeOf(data))
+	//fmt.Println(data)
+	switch reflect.TypeOf(data).Kind() {
+	case reflect.Map:
+		mapV := reflect.ValueOf(data)
+		keysV := mapV.MapKeys()
+		for key := range keysV {
+			result = append(result, keysV[key].String())
+		}
+	case reflect.Array, reflect.Slice:
+		sliceV := reflect.ValueOf(data)
+		for idx := 0; idx < sliceV.Len(); idx++ {
+			result = append(result, fmt.Sprintf("%d", idx))
+		}
+	case reflect.Struct:
+		result = structs.Names(data)
+	}
+	return result
+}
+
+func inside(possible []string, target string) bool {
+	for _, val := range possible {
+		if target == val {
+			return true
+		}
+	}
+	return false
+}
+
+func next_level(current_level interface{}, go_to string) (interface{}, error) {
+	if inside(GetPossible(current_level), go_to) {
+		//fmt.Printf("%v (%v)\r\n", reflect.TypeOf(current_level).Kind(), reflect.TypeOf(current_level))
+		switch reflect.TypeOf(current_level).Kind() {
+		case reflect.Map:
+			CL, err := mapToInterface(current_level)
+			if err != nil {
+				return nil, err
+			}
+			return CL[go_to], nil
+		case reflect.Array, reflect.Slice:
+			val, err := strconv.Atoi(go_to)
+			if err == nil {
+				CL, err := sliceToInterface(current_level)
+				if err != nil {
+					return nil, err
+				}
+				return CL[val], nil
+			} else {
+				return nil, err
+			}
+		case reflect.Struct:
+			structV := reflect.ValueOf(current_level)
+			return structV.FieldByName(go_to).Interface(), nil
+		}
+	}
+	return nil, fmt.Errorf("Failed moving to '%s' from '%s' (%v)", go_to, current_level, reflect.TypeOf(current_level))
+}
+
+func list_possible(possible []string) []string {
+	var result []string
+	for _, val := range possible {
+		result = append(result, fmt.Sprintf("'%s'", val))
+	}
+	return result
+}
+
+func Glom(data interface{}, path string) (interface{}, error) {
+	complete_path := strings.Split(path, ".")
+	//fmt.Printf("Seeking '%s' will take %d steps\r\n", path, len(complete_path))
+	var path_taken []string
+	var currently interface{}
+	currently = data
+	for _, hop := range complete_path {
+		//fmt.Printf("current: %v\r\n", currently)
+		//fmt.Printf("Path: '%v'\r\n", strings.Join(path_taken, "."))
+		if hop != "*" && !inside(GetPossible(currently), hop) {
+			return nil, fmt.Errorf("Failed moving to '%s' from path of '%s', options are %s (%d)", hop, strings.Join(path_taken, "."), strings.Join(list_possible(GetPossible(currently)), ", "), len(GetPossible(currently)))
+		} else {
+			if hop != "*" {
+				next, err := next_level(currently, hop)
+				if err != nil {
+					//return nil, fmt.Errorf("Failed moving to '%s' from path of '%s', options are %s (%d)", hop, strings.Join(path_taken, "."), strings.Join(list_possible(GetPossible(next)), ", "), len(GetPossible(next)))
+					return nil, err
+				} else {
+					path_taken = append(path_taken, hop)
+					currently = next
+				}
+			} else {
+				return currently, nil
+			}
+		}
+	}
+	return currently, nil
+}

+ 247 - 0
glom/glom_test.go

@@ -0,0 +1,247 @@
+package glom
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+)
+
+func TestGlomArray(t *testing.T) {
+	var data []interface{}
+	data = append(data, "Goose")
+
+	test1 := make(map[string]interface{})
+
+	test1a := make(map[string]interface{})
+	test1a["name"] = "Ducky"
+	test1a["age"] = 62
+	test1a["race"] = "Duck"
+
+	test1b := make(map[string]interface{})
+	test1b["name"] = "Sir Meow"
+	test1b["age"] = 12
+	test1b["race"] = "Cat"
+
+	var animals []interface{}
+	animals = append(animals, test1a)
+	animals = append(animals, test1b)
+
+	test1["animals"] = animals
+	data = append(data, test1)
+
+	/*
+		HJSON Representation of data
+		data = [
+			"Goose"
+			{
+				"animals": [
+					{"name": "Ducky", "age": 62, "race": "Duck"}
+					{"name": "Sir Meow", "age": 12, "race": "Cat"}
+				]
+			}
+		]
+	*/
+	test, err := Glom(data, "1.animals.1.name")
+	if err != nil {
+		t.Errorf("Unexpected Error: \"%v\"", err)
+	} else if test != "Sir Meow" {
+		t.Errorf("Failed getting 'Sir Meow' got \"%v\"", test)
+	}
+}
+
+func TestGetPossible(t *testing.T) {
+	var data []string
+	data = append(data, "One")
+	data = append(data, "Two")
+	data = append(data, "Three")
+	data = append(data, "Four")
+
+	result := getPossible(data)
+	if len(result) != len(data) {
+		t.Errorf("Expected even size, %d != %d", len(result), len(data))
+	}
+}
+
+func TestStruct(t *testing.T) {
+	type Animal struct {
+		Name     string
+		Lifespan int
+	}
+
+	cat := Animal{"Cat", 12}
+	dog := Animal{"Dog", 13}
+
+	var data []Animal
+	data = append(data, cat)
+	data = append(data, dog)
+
+	test, err := Glom(data, "1.*")
+	if err != nil {
+		t.Errorf("TestStruct 1/3: Unexpected Error: \"%v\"", err)
+	} else {
+		if fmt.Sprintf("%v", test) != fmt.Sprintf("%v", dog) {
+			t.Errorf("TestStruct 1/3: Failed getting '%v' got '%v'", dog, test)
+		}
+	}
+
+	test2, err2 := Glom(cat, "Lifespan")
+	if err2 != nil {
+		t.Errorf("TestStruct 2/3: Unexpected Error: \"%v\"", err2)
+	} else {
+		if test2 != cat.Lifespan {
+			t.Errorf("TestStruct 2/3: Failed getting '%v' got '%v'", cat.Lifespan, test2)
+		}
+	}
+
+	data2 := make(map[string]Animal)
+
+	data2["Squirrel"] = Animal{"Squirrel", 999}
+	data2["Hamster"] = Animal{"Hamster", 4}
+
+	test3, err3 := Glom(data2, "Squirrel.Name")
+	if err3 != nil {
+		t.Errorf("TestStruct 3/3: Unexpected Error: \"%v\"", err3)
+	} else {
+		if test3 != "Squirrel" {
+			t.Errorf("TestStruct 3/3: Failed getting 'Squirrel' got '%v'", test3)
+		}
+	}
+}
+
+func TestListPossible(t *testing.T) {
+	var list []string
+	list = append(list, "One")
+	list = append(list, "Two")
+	list = append(list, "Three")
+
+	result := list_possible(list)
+
+	if strings.Join(result, ", ") != "'One', 'Two', 'Three'" {
+		t.Errorf("Failed getting \"%s\" got \"%v\"", "'One', 'Two', 'Three'", strings.Join(result, ", "))
+	}
+}
+
+func TestFail(t *testing.T) {
+	data := make(map[string]interface{})
+	data["Duck"] = "Quack"
+	data["Cheese"] = 3
+	data["Mouse"] = true
+
+	test, err := Glom(data, "Moose")
+	if err == nil {
+		t.Errorf("Expected Error, got '%v'", test)
+	}
+}
+
+func TestMapToInter(t *testing.T) {
+	m := make(map[string]string)
+	m["Duck"] = "Quack"
+	m["Cheese"] = "Yes Please!"
+	m["Mouse"] = "true"
+	var s []string
+	s = append(s, "Duck")
+	s = append(s, "Cheese")
+	s = append(s, "Mouse")
+	var m2 map[string]int
+
+	_, err1 := mapToInterface(m)
+	if err1 != nil {
+		t.Errorf("Unexpected Error given map: %v", err1)
+	}
+
+	test2, err2 := mapToInterface(s)
+	if err2 == nil {
+		t.Errorf("Expected Error given slice, got '%v'", test2)
+	}
+
+	test3, err3 := mapToInterface(m2)
+	if err3 == nil {
+		t.Errorf("Expected Error given invalid/empty map, got '%v'", test3)
+	}
+}
+
+func TestSliceToInter(t *testing.T) {
+	m := make(map[string]string)
+	m["Duck"] = "Quack"
+	m["Cheese"] = "Yes Please!"
+	m["Mouse"] = "true"
+	var s []string
+	s = append(s, "Duck")
+	s = append(s, "Cheese")
+	s = append(s, "Mouse")
+	var m2 map[string]int
+	var s2 []int
+
+	test1, err1 := sliceToInterface(m)
+	if err1 == nil {
+		t.Errorf("Expected Error given map, got '%v'", test1)
+	}
+
+	_, err2 := sliceToInterface(s)
+	if err2 != nil {
+		t.Errorf("Unexpected Error given slice: %v", err2)
+	}
+
+	test3, err3 := sliceToInterface(m2)
+	if err3 == nil {
+		t.Errorf("Expected Error given invalid/empty map, got '%v'", test3)
+	}
+
+	test4, err4 := sliceToInterface(s2)
+	if err4 == nil {
+		t.Errorf("Expected Error given invalid/empty slice, got '%v'", test4)
+	}
+}
+
+func TestEdgeCasesMapNextLvl(t *testing.T) {
+	// This doesn't work, I thought it would but it does not
+	var m map[string]int
+	m2 := make(map[string]int)
+	m2["Cheese"] = 6
+	m2["C"] = 1
+	m2["h"] = 1
+	m2["e"] = 3
+	m2["s"] = 1
+
+	test1, err1 := next_level(m, "failwhale")
+	if err1 == nil {
+		t.Errorf("Expected Error given invalid/empty map, got '%v'", test1)
+	}
+
+	test2, err2 := next_level(m2, "n")
+	if err2 == nil {
+		t.Errorf("Expected Error given map but invalid key, got '%v'", test2)
+	}
+}
+
+func TestEdgeCasesGlom(t *testing.T) {
+	// This is just a generic test, nothing fancy
+	data := make(map[string]interface{})
+
+	lvl2 := make(map[string]interface{})
+	lvl2["Duck"] = "Quack"
+	lvl2["Cheese"] = 6
+	lvl2["Mouse"] = true
+	data["part1"] = lvl2
+
+	var lvl1 []interface{}
+	lvl1 = append(lvl1, "Pig")
+	lvl1 = append(lvl1, "Chicken")
+	lvl1 = append(lvl1, "Cow")
+	lvl1 = append(lvl1, "Dog")
+	lvl1 = append(lvl1, "Cat")
+	lvl1 = append(lvl1, "Horse")
+	lvl1 = append(lvl1, true)
+	lvl1 = append(lvl1, 42)
+	data["part2"] = lvl1
+
+	_, err1 := Glom(data, "part1.Mouse")
+	if err1 != nil {
+		t.Errorf("Unexpected Error (part1.Mouse = true): %v", err1)
+	}
+
+	_, err2 := Glom(data, "part2.3")
+	if err2 != nil {
+		t.Errorf("Unexpected Error (part2.3 = 'Dog'): %v", err2)
+	}
+}

+ 6 - 0
glom/go.mod

@@ -0,0 +1,6 @@
+module red-green/glom
+
+go 1.17
+
+require github.com/fatih/structs v1.1.0 // indirect
+

+ 2 - 0
glom/go.sum

@@ -0,0 +1,2 @@
+github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=

+ 8 - 0
go.mod

@@ -1,3 +1,11 @@
 module red-green/go-roku
 
 go 1.17
+
+require (
+	github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542 // indirect
+	github.com/fatih/structs v1.1.0 // indirect
+	red-green/glom v0.0.0-00010101000000-000000000000 // indirect
+)
+
+replace red-green/glom => ./glom

+ 4 - 0
go.sum

@@ -0,0 +1,4 @@
+github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542 h1:nYXb+3jF6Oq/j8R/y90XrKpreCxIalBWfeyeKymgOPk=
+github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU=
+github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=

+ 30 - 0
keys.go

@@ -0,0 +1,30 @@
+package main
+
+func GetKeys() map[string]string {
+	keys := make(map[string]string)
+	keys["home"] = "home"
+	keys["rev"] = "rev"
+	keys["fwd"] = "fwd"
+	keys["left"] = "left"
+	keys["right"] = "right"
+	keys["down"] = "down"
+	keys["up"] = "up"
+	keys["play"] = "play"
+	keys["ok"] = "select"
+	keys["back"] = "back"
+	keys["instant-replay"] = "instantreplay"
+	keys["info"] = "info"
+	keys["backspace"] = "backspace"
+	keys["search"] = "search"
+	keys["enter"] = "enter"
+
+	keys["power"] = "poweroff"
+
+	keys["vol+"] = "volumeup"
+	keys["vol-"] = "volumedown"
+	keys["mute"] = "volumemute"
+
+	keys["chan+"] = "channelup"
+	keys["chan-"] = "channeldown"
+	return keys
+}

+ 95 - 3
poster.go

@@ -1,16 +1,108 @@
 package main
 
 import (
+	"fmt"
+	"io"
 	"log"
 	"net/http"
-	"net/url"
+	"red-green/glom"
+
+	"github.com/beego/x2j"
 )
 
 // https://zetcode.com/golang/getpostrequest/ post_req_form.go
-func Post(where string, data *url.Values) *http.Response {
-	resp, err := http.PostForm(where, *data)
+func Post(where string) *http.Response {
+	resp, err := http.PostForm(where, nil)
 	if err != nil {
 		log.Fatalf("POST got non 200 code, %v", err)
 	}
 	return resp
 }
+
+func Get(where string) *http.Response {
+	resp, err := http.Get(where)
+	if err != nil {
+		log.Fatalf("GET got non 200 code, %v", err)
+	}
+	return resp
+}
+
+// https://developer.roku.com/docs/developer-program/debugging/external-control-api.md#keypress-key-values codes
+func KeyPress(ip, command string) string {
+	return fmt.Sprintf("http://%s:8060/keypress/%s", ip, command)
+}
+
+// https://developer.roku.com/docs/developer-program/debugging/external-control-api.md#general-ecp-commands
+func Query(ip, what string) string {
+	return fmt.Sprintf("http://%s:8060/query/%s", ip, what)
+}
+
+func PerformKey(ip, command string) {
+	Post(KeyPress(ip, command))
+}
+
+func GetCurrentPlay(ip string) map[string]interface{} {
+	resp := Get(Query(ip, "media-player"))
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		log.Panicf("Reading from body got error, %v", err)
+	}
+	r := make(map[string]interface{})
+	err = x2j.Unmarshal(body, &r)
+	if err != nil {
+		log.Panicf("Got a error parsing XML, %v", err)
+	}
+	return r
+}
+
+func GetQuery(ip, command string) map[string]interface{} {
+	resp := Get(Query(ip, command))
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		log.Panicf("Reading from body got error, %v", err)
+	}
+	r := make(map[string]interface{})
+	err = x2j.Unmarshal(body, &r)
+	if err != nil {
+		log.Panicf("Got a error parsing XML, %v", err)
+	}
+	return r
+}
+
+func main() {
+	keys := GetKeys()
+	_ = keys // Keep alive
+	PerformKey("192.168.254.75", keys["mute"])
+	//Post("http://192.168.254.75:8060/launch/74519")
+	//PerformKey("192.168.254.75", keys["home"])
+
+	/*
+		//resp := Post("http://192.168.254.75:8060/keypress/volumeMute")
+		//resp := Post(KeyPress("192.168.254.75", "volumeMute"))
+		//resp := Get(Query("192.168.254.75", "device-info"))
+		//resp := GetQuery("192.168.254.75", "tv-active-channel")
+		resp := GetQuery("192.168.254.75", "tv-channels")
+
+		//result, err := glom.Glom(resp, "tv-channels.channel")
+		_, err := glom.Glom(resp, "*")
+		if err != nil {
+			log.Println(err)
+		} else {
+			//fmt.Println(result)
+			fmt.Println(glom.GetPossible(result))
+		}
+	*/
+
+	// Query the current playing thing, and get the tv channels
+	r := GetQuery("192.168.254.75", "media-player")
+	name, _ := glom.Glom(r, "player.plugin.-name")
+	id, _ := glom.Glom(r, "player.plugin.-id")
+	r1 := GetQuery("192.168.254.75", "tv-channels")
+	chan_name, _ := glom.Glom(r1, "tv-channels.channel.name")
+	chan_phy_id, _ := glom.Glom(r1, "tv-channels.channel.physical-channel")
+	chan_number, _ := glom.Glom(r1, "tv-channels.channel.number")
+
+	// Debug print
+	fmt.Printf("Name: %v (%v)\r\n", name, id)
+	fmt.Printf("Channel: %v (%v / %v)\r\n", chan_name, chan_phy_id, chan_number)
+}