Skip to content

Commit

Permalink
feat: device config, rebased changes, add fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
bdrich committed Feb 18, 2021
1 parent 0553f55 commit 64860a8
Showing 1 changed file with 206 additions and 32 deletions.
238 changes: 206 additions & 32 deletions groku.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io/ioutil"
"net"
Expand All @@ -11,6 +12,8 @@ import (
"os"
"strings"
"time"

"github.com/mitchellh/go-homedir"
)

var CONFIG string
Expand Down Expand Up @@ -38,30 +41,60 @@ Commands:
replay Replay
play Play
pause Pause
discover Discover a roku on your local network
discover Discover Roku devices on your local network
list List known Roku devices
use Set Roku name to use
device-info Display device info
text Send text to the Roku
apps List installed apps on your Roku
app Launch specified app
on Power On
off Power Off
volup Volume Up
voldown Volume Down
mute Volume Mute/Unmute
`
)

type dictonary struct {
type dictionary struct {
XMLName xml.Name `xml:"apps"`
Apps []app `xml:"app"`
}

type deviceinfo struct {
XMLName xml.Name `xml:"device-info"`
UDN string `xml:"udn"`
Serial string `xml:"serial-number"`
DeviceID string `xml:"device-id"`
ModelNum string `xml:"model-number"`
ModelName string `xml:"model-name"`
DeviceName string `xml:"user-device-name"`
}

type roku struct {
Address string `json:"address"`
Name string `json:"name"`
}

type app struct {
Name string `xml:",chardata"`
ID string `xml:"id,attr"`
}

type grokuConfig struct {
Address string `json:"address"`
LastName string `json:"lastname"`
Current roku `json:"current"`
Rokus []roku `json:"rokus"`
Timestamp int64 `json:"timestamp"`
}

func main() {
CONFIG = fmt.Sprintf("%s/groku.json", os.TempDir())
home, err := homedir.Dir()
if err != nil {
fmt.Println("Cannot find home directory")
os.Exit(1)
}
CONFIG = fmt.Sprintf("%s/.groku.json", home)

if len(os.Args) == 1 || os.Args[1] == "--help" || os.Args[1] == "-help" ||
os.Args[1] == "--h" || os.Args[1] == "-h" || os.Args[1] == "help" {
Expand All @@ -78,24 +111,66 @@ func main() {
switch os.Args[1] {
case "home", "rev", "fwd", "select", "left", "right", "down", "up",
"back", "info", "backspace", "enter", "search":
http.PostForm(fmt.Sprintf("%vkeypress/%v", getRokuAddress(), os.Args[1]), nil)
http.PostForm(fmt.Sprintf("%vkeypress/%v", getCurrentRokuAddress(), os.Args[1]), nil)
os.Exit(0)
case "replay":
http.PostForm(fmt.Sprintf("%vkeypress/%v", getRokuAddress(), "InstantReplay"), nil)
http.PostForm(fmt.Sprintf("%vkeypress/%v", getCurrentRokuAddress(), "InstantReplay"), nil)
os.Exit(0)
case "play", "pause":
http.PostForm(fmt.Sprintf("%vkeypress/%v", getRokuAddress(), "Play"), nil)
http.PostForm(fmt.Sprintf("%vkeypress/%v", getCurrentRokuAddress(), "Play"), nil)
os.Exit(0)
case "volup", "voldown", "mute":
http.PostForm(fmt.Sprintf("%vkeypress/Volume%v", getCurrentRokuAddress(), strings.TrimPrefix(os.Args[1], "vol")), nil)
os.Exit(0)
case "off", "on":
http.PostForm(fmt.Sprintf("%vkeypress/Power%v", getCurrentRokuAddress(), os.Args[1]), nil)
os.Exit(0)
case "discover":
fmt.Println("Found roku at", getRokuAddress())
config := getRokuConfig()
if len(config.Rokus) > 0 {
for _, r := range config.Rokus {
fmt.Print("Found roku at ", r.Address)
if r.Name != "" {
fmt.Print(" named ", r.Name)
}
fmt.Println()
}
}
os.Exit(0)
case "list":
config := getRokuConfig()
for _, r := range config.Rokus {
if r.Name != "" {
fmt.Print(r.Name, ": ")
}
fmt.Println(r.Address)
}
case "use":
config := getRokuConfig()
for _, r := range config.Rokus {
if strings.ToUpper(os.Args[2]) == strings.ToUpper(r.Name) {
config.Current = r
config.LastName = os.Args[2]
writeConfig(config)
fmt.Printf("Using Roku named %v at %v\n", r.Name, r.Address)
os.Exit(0)
}
}
fmt.Printf("Cannot find Roku named %v\n", os.Args[2])
case "device-info":
info, err := queryInfo()
if err == nil && getCurrentRokuName() != "" {
fmt.Printf("Name:\t\t%v\n", info.DeviceName)
}
fmt.Printf("Model:\t\t%v %v\n", info.ModelName, info.ModelNum)
fmt.Printf("Serial:\t\t%v\n", info.Serial)
case "text":
if len(os.Args) < 3 {
fmt.Println(USAGE)
os.Exit(1)
}

roku := getRokuAddress()
roku := getCurrentRokuAddress()
for _, c := range os.Args[2] {
http.PostForm(fmt.Sprintf("%skeypress/Lit_%s", roku, url.QueryEscape(string(c))), nil)
}
Expand All @@ -116,7 +191,7 @@ func main() {

for _, a := range dict.Apps {
if a.Name == os.Args[2] {
http.PostForm(fmt.Sprintf("%vlaunch/%v", getRokuAddress(), a.ID), nil)
http.PostForm(fmt.Sprintf("%vlaunch/%v", getCurrentRokuAddress(), a.ID), nil)
os.Exit(0)
}
}
Expand All @@ -128,16 +203,16 @@ func main() {
}
}

func queryApps() dictonary {
resp, err := http.Get(fmt.Sprintf("%squery/apps", getRokuAddress()))
func queryApps() dictionary {
resp, err := http.Get(fmt.Sprintf("%squery/apps", getCurrentRokuAddress()))
if err != nil {
fmt.Println(err)
os.Exit(1)
}

defer resp.Body.Close()

var dict dictonary
var dict dictionary
if err := xml.NewDecoder(resp.Body).Decode(&dict); err != nil {
fmt.Println(err)
os.Exit(1)
Expand All @@ -146,7 +221,29 @@ func queryApps() dictonary {
return dict
}

func findRoku() string {
func queryInfoForAddress(address string) (deviceinfo, error) {
resp, err := http.Get(fmt.Sprintf("%squery/device-info", address))
var info deviceinfo
if err != nil {
fmt.Println(err)
return info, err
}

defer resp.Body.Close()

if err := xml.NewDecoder(resp.Body).Decode(&info); err != nil {
fmt.Println(err)
return info, err
}

return info, err
}

func queryInfo() (deviceinfo, error) {
return queryInfoForAddress(getCurrentRokuAddress())
}

func findRokus() []roku {
ssdp, err := net.ResolveUDPAddr("udp", "239.255.255.250:1900")
if err != nil {
fmt.Println(err)
Expand Down Expand Up @@ -176,51 +273,128 @@ func findRoku() string {
os.Exit(1)
}

answerBytes := make([]byte, 1024)
err = socket.SetReadDeadline(time.Now().Add(3 * time.Second))
if err != nil {
fmt.Println(err)
os.Exit(1)
var rokus []roku
listentimer := time.Now().Add(5 * time.Second)
for time.Now().Before(listentimer) {
answerBytes := make([]byte, 1024)
err = socket.SetReadDeadline(listentimer)
if err != nil {
fmt.Println(err)
}
_, _, err = socket.ReadFromUDP(answerBytes[:])

if err == nil {
ret := strings.Split(string(answerBytes), "\r\n")
location := strings.TrimPrefix(ret[6], "LOCATION: ")
id := ret[3]
if !strings.Contains(id, "roku") {
continue
}
info, err := queryInfoForAddress(location)
if err == nil {
duplicateDeviceEntry := false
for _, r := range rokus {
if r.Address == location {
duplicateDeviceEntry = true
fmt.Printf("device already in list %s\n", info.DeviceName)
break
}
}
if !duplicateDeviceEntry {
r := roku{Name: info.DeviceName, Address: location}
rokus = append(rokus, r)
}
}
}
}
return rokus
}

_, _, err = socket.ReadFromUDP(answerBytes[:])
if err != nil {
fmt.Println("Could not find your Roku!")
os.Exit(1)
}
func getCurrentRokuAddress() string {
return getRokuConfig().Current.Address
}

ret := strings.Split(string(answerBytes), "\r\n")
location := strings.TrimPrefix(ret[6], "LOCATION: ")
func getCurrentRokuName() string {
return getRokuConfig().Current.Name
}

return location
func getRokuConfigFor(name string) (*roku, error) {
config := getRokuConfig()
for _, e := range config.Rokus {
if strings.ToUpper(e.Name) == strings.ToUpper(name) {
return &e, nil
}
}
return nil, errors.New(fmt.Sprintf("%v not found", name))
}

func getRokuAddress() string {
func getRokuConfig() grokuConfig {
var configFile *os.File
var config grokuConfig

configFile, err := os.Open(CONFIG)

// the config file doesn't exist, but that's okay
if err != nil {
config.Address = findRoku()
config.Rokus = findRokus()
config.Timestamp = time.Now().Unix()
} else {
// the config file exists
if err := json.NewDecoder(configFile).Decode(&config); err != nil {
config.Address = findRoku()
config.Rokus = findRokus()
}

//if the config file is over 60 seconds old, then replace it
if config.Timestamp == 0 || time.Now().Unix()-config.Timestamp > 60 {
config.Address = findRoku()
config.Rokus = findRokus()
config.Timestamp = time.Now().Unix()
}
}
if len(config.Rokus) == 0 {
fmt.Println("No rokus found")
os.Exit(1)
}
if config.LastName != "" {
found := false
for _, e := range config.Rokus {
if strings.ToUpper(e.Name) == strings.ToUpper(config.LastName) {
config.Current = e
found = true
}
}
if !found && len(config.Rokus) > 0 {
config.Current = config.Rokus[0]
fmt.Printf("Previously used Roku %v not found anymore, using %v as new default\n", config.LastName, config.Current.Name)
}
} else {
config.Current = config.Rokus[0]
}
writeConfig(config)
return config
}

func writeConfig(config grokuConfig) error {
var oldConfig grokuConfig
oldConfigBytes, err := ioutil.ReadFile(CONFIG)
if err == nil {
json.Unmarshal(oldConfigBytes, &oldConfig)
}
configRokus := []roku{}
if oldConfigBytes != nil {
for _, newR := range config.Rokus {
thisRoku := newR
for _, oldR := range oldConfig.Rokus {
if oldR.Address == newR.Address {
thisRoku = oldR
}
}
configRokus = append(configRokus, thisRoku)
}
config.Rokus = configRokus
}
if b, err := json.Marshal(config); err == nil {
ioutil.WriteFile(CONFIG, b, os.ModePerm)
}

return config.Address
return nil
}

0 comments on commit 64860a8

Please sign in to comment.