Skip to content

Commit 64860a8

Browse files
committed
feat: device config, rebased changes, add fixes
1 parent 0553f55 commit 64860a8

File tree

1 file changed

+206
-32
lines changed

1 file changed

+206
-32
lines changed

groku.go

Lines changed: 206 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"encoding/json"
55
"encoding/xml"
6+
"errors"
67
"fmt"
78
"io/ioutil"
89
"net"
@@ -11,6 +12,8 @@ import (
1112
"os"
1213
"strings"
1314
"time"
15+
16+
"github.com/mitchellh/go-homedir"
1417
)
1518

1619
var CONFIG string
@@ -38,30 +41,60 @@ Commands:
3841
replay Replay
3942
play Play
4043
pause Pause
41-
discover Discover a roku on your local network
44+
discover Discover Roku devices on your local network
45+
list List known Roku devices
46+
use Set Roku name to use
47+
device-info Display device info
4248
text Send text to the Roku
4349
apps List installed apps on your Roku
4450
app Launch specified app
51+
on Power On
52+
off Power Off
53+
volup Volume Up
54+
voldown Volume Down
55+
mute Volume Mute/Unmute
4556
`
4657
)
4758

48-
type dictonary struct {
59+
type dictionary struct {
4960
XMLName xml.Name `xml:"apps"`
5061
Apps []app `xml:"app"`
5162
}
5263

64+
type deviceinfo struct {
65+
XMLName xml.Name `xml:"device-info"`
66+
UDN string `xml:"udn"`
67+
Serial string `xml:"serial-number"`
68+
DeviceID string `xml:"device-id"`
69+
ModelNum string `xml:"model-number"`
70+
ModelName string `xml:"model-name"`
71+
DeviceName string `xml:"user-device-name"`
72+
}
73+
74+
type roku struct {
75+
Address string `json:"address"`
76+
Name string `json:"name"`
77+
}
78+
5379
type app struct {
5480
Name string `xml:",chardata"`
5581
ID string `xml:"id,attr"`
5682
}
5783

5884
type grokuConfig struct {
59-
Address string `json:"address"`
85+
LastName string `json:"lastname"`
86+
Current roku `json:"current"`
87+
Rokus []roku `json:"rokus"`
6088
Timestamp int64 `json:"timestamp"`
6189
}
6290

6391
func main() {
64-
CONFIG = fmt.Sprintf("%s/groku.json", os.TempDir())
92+
home, err := homedir.Dir()
93+
if err != nil {
94+
fmt.Println("Cannot find home directory")
95+
os.Exit(1)
96+
}
97+
CONFIG = fmt.Sprintf("%s/.groku.json", home)
6598

6699
if len(os.Args) == 1 || os.Args[1] == "--help" || os.Args[1] == "-help" ||
67100
os.Args[1] == "--h" || os.Args[1] == "-h" || os.Args[1] == "help" {
@@ -78,24 +111,66 @@ func main() {
78111
switch os.Args[1] {
79112
case "home", "rev", "fwd", "select", "left", "right", "down", "up",
80113
"back", "info", "backspace", "enter", "search":
81-
http.PostForm(fmt.Sprintf("%vkeypress/%v", getRokuAddress(), os.Args[1]), nil)
114+
http.PostForm(fmt.Sprintf("%vkeypress/%v", getCurrentRokuAddress(), os.Args[1]), nil)
82115
os.Exit(0)
83116
case "replay":
84-
http.PostForm(fmt.Sprintf("%vkeypress/%v", getRokuAddress(), "InstantReplay"), nil)
117+
http.PostForm(fmt.Sprintf("%vkeypress/%v", getCurrentRokuAddress(), "InstantReplay"), nil)
85118
os.Exit(0)
86119
case "play", "pause":
87-
http.PostForm(fmt.Sprintf("%vkeypress/%v", getRokuAddress(), "Play"), nil)
120+
http.PostForm(fmt.Sprintf("%vkeypress/%v", getCurrentRokuAddress(), "Play"), nil)
121+
os.Exit(0)
122+
case "volup", "voldown", "mute":
123+
http.PostForm(fmt.Sprintf("%vkeypress/Volume%v", getCurrentRokuAddress(), strings.TrimPrefix(os.Args[1], "vol")), nil)
124+
os.Exit(0)
125+
case "off", "on":
126+
http.PostForm(fmt.Sprintf("%vkeypress/Power%v", getCurrentRokuAddress(), os.Args[1]), nil)
88127
os.Exit(0)
89128
case "discover":
90-
fmt.Println("Found roku at", getRokuAddress())
129+
config := getRokuConfig()
130+
if len(config.Rokus) > 0 {
131+
for _, r := range config.Rokus {
132+
fmt.Print("Found roku at ", r.Address)
133+
if r.Name != "" {
134+
fmt.Print(" named ", r.Name)
135+
}
136+
fmt.Println()
137+
}
138+
}
91139
os.Exit(0)
140+
case "list":
141+
config := getRokuConfig()
142+
for _, r := range config.Rokus {
143+
if r.Name != "" {
144+
fmt.Print(r.Name, ": ")
145+
}
146+
fmt.Println(r.Address)
147+
}
148+
case "use":
149+
config := getRokuConfig()
150+
for _, r := range config.Rokus {
151+
if strings.ToUpper(os.Args[2]) == strings.ToUpper(r.Name) {
152+
config.Current = r
153+
config.LastName = os.Args[2]
154+
writeConfig(config)
155+
fmt.Printf("Using Roku named %v at %v\n", r.Name, r.Address)
156+
os.Exit(0)
157+
}
158+
}
159+
fmt.Printf("Cannot find Roku named %v\n", os.Args[2])
160+
case "device-info":
161+
info, err := queryInfo()
162+
if err == nil && getCurrentRokuName() != "" {
163+
fmt.Printf("Name:\t\t%v\n", info.DeviceName)
164+
}
165+
fmt.Printf("Model:\t\t%v %v\n", info.ModelName, info.ModelNum)
166+
fmt.Printf("Serial:\t\t%v\n", info.Serial)
92167
case "text":
93168
if len(os.Args) < 3 {
94169
fmt.Println(USAGE)
95170
os.Exit(1)
96171
}
97172

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

117192
for _, a := range dict.Apps {
118193
if a.Name == os.Args[2] {
119-
http.PostForm(fmt.Sprintf("%vlaunch/%v", getRokuAddress(), a.ID), nil)
194+
http.PostForm(fmt.Sprintf("%vlaunch/%v", getCurrentRokuAddress(), a.ID), nil)
120195
os.Exit(0)
121196
}
122197
}
@@ -128,16 +203,16 @@ func main() {
128203
}
129204
}
130205

131-
func queryApps() dictonary {
132-
resp, err := http.Get(fmt.Sprintf("%squery/apps", getRokuAddress()))
206+
func queryApps() dictionary {
207+
resp, err := http.Get(fmt.Sprintf("%squery/apps", getCurrentRokuAddress()))
133208
if err != nil {
134209
fmt.Println(err)
135210
os.Exit(1)
136211
}
137212

138213
defer resp.Body.Close()
139214

140-
var dict dictonary
215+
var dict dictionary
141216
if err := xml.NewDecoder(resp.Body).Decode(&dict); err != nil {
142217
fmt.Println(err)
143218
os.Exit(1)
@@ -146,7 +221,29 @@ func queryApps() dictonary {
146221
return dict
147222
}
148223

149-
func findRoku() string {
224+
func queryInfoForAddress(address string) (deviceinfo, error) {
225+
resp, err := http.Get(fmt.Sprintf("%squery/device-info", address))
226+
var info deviceinfo
227+
if err != nil {
228+
fmt.Println(err)
229+
return info, err
230+
}
231+
232+
defer resp.Body.Close()
233+
234+
if err := xml.NewDecoder(resp.Body).Decode(&info); err != nil {
235+
fmt.Println(err)
236+
return info, err
237+
}
238+
239+
return info, err
240+
}
241+
242+
func queryInfo() (deviceinfo, error) {
243+
return queryInfoForAddress(getCurrentRokuAddress())
244+
}
245+
246+
func findRokus() []roku {
150247
ssdp, err := net.ResolveUDPAddr("udp", "239.255.255.250:1900")
151248
if err != nil {
152249
fmt.Println(err)
@@ -176,51 +273,128 @@ func findRoku() string {
176273
os.Exit(1)
177274
}
178275

179-
answerBytes := make([]byte, 1024)
180-
err = socket.SetReadDeadline(time.Now().Add(3 * time.Second))
181-
if err != nil {
182-
fmt.Println(err)
183-
os.Exit(1)
276+
var rokus []roku
277+
listentimer := time.Now().Add(5 * time.Second)
278+
for time.Now().Before(listentimer) {
279+
answerBytes := make([]byte, 1024)
280+
err = socket.SetReadDeadline(listentimer)
281+
if err != nil {
282+
fmt.Println(err)
283+
}
284+
_, _, err = socket.ReadFromUDP(answerBytes[:])
285+
286+
if err == nil {
287+
ret := strings.Split(string(answerBytes), "\r\n")
288+
location := strings.TrimPrefix(ret[6], "LOCATION: ")
289+
id := ret[3]
290+
if !strings.Contains(id, "roku") {
291+
continue
292+
}
293+
info, err := queryInfoForAddress(location)
294+
if err == nil {
295+
duplicateDeviceEntry := false
296+
for _, r := range rokus {
297+
if r.Address == location {
298+
duplicateDeviceEntry = true
299+
fmt.Printf("device already in list %s\n", info.DeviceName)
300+
break
301+
}
302+
}
303+
if !duplicateDeviceEntry {
304+
r := roku{Name: info.DeviceName, Address: location}
305+
rokus = append(rokus, r)
306+
}
307+
}
308+
}
184309
}
310+
return rokus
311+
}
185312

186-
_, _, err = socket.ReadFromUDP(answerBytes[:])
187-
if err != nil {
188-
fmt.Println("Could not find your Roku!")
189-
os.Exit(1)
190-
}
313+
func getCurrentRokuAddress() string {
314+
return getRokuConfig().Current.Address
315+
}
191316

192-
ret := strings.Split(string(answerBytes), "\r\n")
193-
location := strings.TrimPrefix(ret[6], "LOCATION: ")
317+
func getCurrentRokuName() string {
318+
return getRokuConfig().Current.Name
319+
}
194320

195-
return location
321+
func getRokuConfigFor(name string) (*roku, error) {
322+
config := getRokuConfig()
323+
for _, e := range config.Rokus {
324+
if strings.ToUpper(e.Name) == strings.ToUpper(name) {
325+
return &e, nil
326+
}
327+
}
328+
return nil, errors.New(fmt.Sprintf("%v not found", name))
196329
}
197330

198-
func getRokuAddress() string {
331+
func getRokuConfig() grokuConfig {
199332
var configFile *os.File
200333
var config grokuConfig
201334

202335
configFile, err := os.Open(CONFIG)
203336

204337
// the config file doesn't exist, but that's okay
205338
if err != nil {
206-
config.Address = findRoku()
339+
config.Rokus = findRokus()
207340
config.Timestamp = time.Now().Unix()
208341
} else {
209342
// the config file exists
210343
if err := json.NewDecoder(configFile).Decode(&config); err != nil {
211-
config.Address = findRoku()
344+
config.Rokus = findRokus()
212345
}
213346

214347
//if the config file is over 60 seconds old, then replace it
215348
if config.Timestamp == 0 || time.Now().Unix()-config.Timestamp > 60 {
216-
config.Address = findRoku()
349+
config.Rokus = findRokus()
217350
config.Timestamp = time.Now().Unix()
218351
}
219352
}
353+
if len(config.Rokus) == 0 {
354+
fmt.Println("No rokus found")
355+
os.Exit(1)
356+
}
357+
if config.LastName != "" {
358+
found := false
359+
for _, e := range config.Rokus {
360+
if strings.ToUpper(e.Name) == strings.ToUpper(config.LastName) {
361+
config.Current = e
362+
found = true
363+
}
364+
}
365+
if !found && len(config.Rokus) > 0 {
366+
config.Current = config.Rokus[0]
367+
fmt.Printf("Previously used Roku %v not found anymore, using %v as new default\n", config.LastName, config.Current.Name)
368+
}
369+
} else {
370+
config.Current = config.Rokus[0]
371+
}
372+
writeConfig(config)
373+
return config
374+
}
220375

376+
func writeConfig(config grokuConfig) error {
377+
var oldConfig grokuConfig
378+
oldConfigBytes, err := ioutil.ReadFile(CONFIG)
379+
if err == nil {
380+
json.Unmarshal(oldConfigBytes, &oldConfig)
381+
}
382+
configRokus := []roku{}
383+
if oldConfigBytes != nil {
384+
for _, newR := range config.Rokus {
385+
thisRoku := newR
386+
for _, oldR := range oldConfig.Rokus {
387+
if oldR.Address == newR.Address {
388+
thisRoku = oldR
389+
}
390+
}
391+
configRokus = append(configRokus, thisRoku)
392+
}
393+
config.Rokus = configRokus
394+
}
221395
if b, err := json.Marshal(config); err == nil {
222396
ioutil.WriteFile(CONFIG, b, os.ModePerm)
223397
}
224398

225-
return config.Address
399+
return nil
226400
}

0 commit comments

Comments
 (0)