@@ -3,6 +3,7 @@ package main
3
3
import (
4
4
"encoding/json"
5
5
"encoding/xml"
6
+ "errors"
6
7
"fmt"
7
8
"io/ioutil"
8
9
"net"
@@ -11,6 +12,8 @@ import (
11
12
"os"
12
13
"strings"
13
14
"time"
15
+
16
+ "github.com/mitchellh/go-homedir"
14
17
)
15
18
16
19
var CONFIG string
@@ -38,30 +41,60 @@ Commands:
38
41
replay Replay
39
42
play Play
40
43
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
42
48
text Send text to the Roku
43
49
apps List installed apps on your Roku
44
50
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
45
56
`
46
57
)
47
58
48
- type dictonary struct {
59
+ type dictionary struct {
49
60
XMLName xml.Name `xml:"apps"`
50
61
Apps []app `xml:"app"`
51
62
}
52
63
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
+
53
79
type app struct {
54
80
Name string `xml:",chardata"`
55
81
ID string `xml:"id,attr"`
56
82
}
57
83
58
84
type grokuConfig struct {
59
- Address string `json:"address"`
85
+ LastName string `json:"lastname"`
86
+ Current roku `json:"current"`
87
+ Rokus []roku `json:"rokus"`
60
88
Timestamp int64 `json:"timestamp"`
61
89
}
62
90
63
91
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 )
65
98
66
99
if len (os .Args ) == 1 || os .Args [1 ] == "--help" || os .Args [1 ] == "-help" ||
67
100
os .Args [1 ] == "--h" || os .Args [1 ] == "-h" || os .Args [1 ] == "help" {
@@ -78,24 +111,66 @@ func main() {
78
111
switch os .Args [1 ] {
79
112
case "home" , "rev" , "fwd" , "select" , "left" , "right" , "down" , "up" ,
80
113
"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 )
82
115
os .Exit (0 )
83
116
case "replay" :
84
- http .PostForm (fmt .Sprintf ("%vkeypress/%v" , getRokuAddress (), "InstantReplay" ), nil )
117
+ http .PostForm (fmt .Sprintf ("%vkeypress/%v" , getCurrentRokuAddress (), "InstantReplay" ), nil )
85
118
os .Exit (0 )
86
119
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 )
88
127
os .Exit (0 )
89
128
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
+ }
91
139
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 )
92
167
case "text" :
93
168
if len (os .Args ) < 3 {
94
169
fmt .Println (USAGE )
95
170
os .Exit (1 )
96
171
}
97
172
98
- roku := getRokuAddress ()
173
+ roku := getCurrentRokuAddress ()
99
174
for _ , c := range os .Args [2 ] {
100
175
http .PostForm (fmt .Sprintf ("%skeypress/Lit_%s" , roku , url .QueryEscape (string (c ))), nil )
101
176
}
@@ -116,7 +191,7 @@ func main() {
116
191
117
192
for _ , a := range dict .Apps {
118
193
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 )
120
195
os .Exit (0 )
121
196
}
122
197
}
@@ -128,16 +203,16 @@ func main() {
128
203
}
129
204
}
130
205
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 ()))
133
208
if err != nil {
134
209
fmt .Println (err )
135
210
os .Exit (1 )
136
211
}
137
212
138
213
defer resp .Body .Close ()
139
214
140
- var dict dictonary
215
+ var dict dictionary
141
216
if err := xml .NewDecoder (resp .Body ).Decode (& dict ); err != nil {
142
217
fmt .Println (err )
143
218
os .Exit (1 )
@@ -146,7 +221,29 @@ func queryApps() dictonary {
146
221
return dict
147
222
}
148
223
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 {
150
247
ssdp , err := net .ResolveUDPAddr ("udp" , "239.255.255.250:1900" )
151
248
if err != nil {
152
249
fmt .Println (err )
@@ -176,51 +273,128 @@ func findRoku() string {
176
273
os .Exit (1 )
177
274
}
178
275
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
+ }
184
309
}
310
+ return rokus
311
+ }
185
312
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
+ }
191
316
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
+ }
194
320
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 ))
196
329
}
197
330
198
- func getRokuAddress () string {
331
+ func getRokuConfig () grokuConfig {
199
332
var configFile * os.File
200
333
var config grokuConfig
201
334
202
335
configFile , err := os .Open (CONFIG )
203
336
204
337
// the config file doesn't exist, but that's okay
205
338
if err != nil {
206
- config .Address = findRoku ()
339
+ config .Rokus = findRokus ()
207
340
config .Timestamp = time .Now ().Unix ()
208
341
} else {
209
342
// the config file exists
210
343
if err := json .NewDecoder (configFile ).Decode (& config ); err != nil {
211
- config .Address = findRoku ()
344
+ config .Rokus = findRokus ()
212
345
}
213
346
214
347
//if the config file is over 60 seconds old, then replace it
215
348
if config .Timestamp == 0 || time .Now ().Unix ()- config .Timestamp > 60 {
216
- config .Address = findRoku ()
349
+ config .Rokus = findRokus ()
217
350
config .Timestamp = time .Now ().Unix ()
218
351
}
219
352
}
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
+ }
220
375
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
+ }
221
395
if b , err := json .Marshal (config ); err == nil {
222
396
ioutil .WriteFile (CONFIG , b , os .ModePerm )
223
397
}
224
398
225
- return config . Address
399
+ return nil
226
400
}
0 commit comments