ruku 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +96 -0
- data/Rakefile +33 -0
- data/bin/ruku +50 -0
- data/lib/ruku/clients/simple.rb +232 -0
- data/lib/ruku/clients/tk.rb +36 -0
- data/lib/ruku/clients/web.rb +117 -0
- data/lib/ruku/clients/web_static/css/ruku.css +196 -0
- data/lib/ruku/clients/web_static/images/box-medium.png +0 -0
- data/lib/ruku/clients/web_static/images/box-small.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/back-over.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/back.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/down-over.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/down.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/fwd-over.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/fwd.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/home-over.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/home.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/left-over.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/left.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/pause-over.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/pause.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/right-over.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/right.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/select-over.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/select.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/space1.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/space2.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/space3.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/up-over.png +0 -0
- data/lib/ruku/clients/web_static/images/remote/up.png +0 -0
- data/lib/ruku/clients/web_static/images/spacer.gif +0 -0
- data/lib/ruku/clients/web_static/index.html +203 -0
- data/lib/ruku/clients/web_static/js/jquery-1.4.2.js +154 -0
- data/lib/ruku/clients/web_static/js/ruku.js +447 -0
- data/lib/ruku/remote.rb +138 -0
- data/lib/ruku/remotes.rb +78 -0
- data/lib/ruku/storage.rb +77 -0
- data/lib/ruku.rb +5 -0
- data/ruku.gemspec +31 -0
- data/test/helper.rb +11 -0
- data/test/js/qunit.css +119 -0
- data/test/js/qunit.js +1069 -0
- data/test/js/runner.html +29 -0
- data/test/js/test_remote.js +37 -0
- data/test/js/test_remote_manager.js +186 -0
- data/test/js/test_remote_menu.js +208 -0
- data/test/js/test_util.js +15 -0
- data/test/test_remote.rb +89 -0
- data/test/test_remotes.rb +144 -0
- data/test/test_simple_client.rb +166 -0
- data/test/test_simple_storage.rb +70 -0
- data/test/test_web_client.rb +46 -0
- data/test/test_yaml_storage.rb +54 -0
- metadata +156 -0
@@ -0,0 +1,447 @@
|
|
1
|
+
function RUKU() {}
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Creates a remote for a particular box. Calling various methods corresponding to remote
|
5
|
+
* commands (i.e. up(), pause(), home()) will make an AJAX request resulting in the command
|
6
|
+
* being sent to the box with the same IP/hostname as the object.
|
7
|
+
*
|
8
|
+
* Initialize with an object with the following properties: host, name, port (optional)
|
9
|
+
*/
|
10
|
+
RUKU.createRemote = function(data) {
|
11
|
+
var remote = {
|
12
|
+
host: data.host,
|
13
|
+
name: data.name,
|
14
|
+
port: data.port || 8080
|
15
|
+
};
|
16
|
+
|
17
|
+
// Add methods to our object, for each of the remote commands, that will make AJAX requests
|
18
|
+
// resulting in the server sending the corresponding remote command to the Roku box with
|
19
|
+
// this remote's IP/hostname.
|
20
|
+
var commands = ["home", "up", "down", "left", "right", "select", "pause", "back", "fwd"];
|
21
|
+
for (var i = 0; i < commands.length; i++) {
|
22
|
+
var cmd = commands[i];
|
23
|
+
remote[cmd] = function(theCmd) {
|
24
|
+
return function() {
|
25
|
+
$.ajax({url:"/ajax", data:{command:theCmd, host:data.host}, success:function(resp) {
|
26
|
+
console.log("Successfully sent command: " + cmd);
|
27
|
+
}, error:function() {
|
28
|
+
console.log("Error on command: " + cmd);
|
29
|
+
}});
|
30
|
+
};
|
31
|
+
}(cmd);
|
32
|
+
}
|
33
|
+
return remote;
|
34
|
+
};
|
35
|
+
|
36
|
+
/**
|
37
|
+
* Creates objects that manage a collection of remote objects. Keeps track of which remote
|
38
|
+
* is the active one, which is the one that commands are to be sent to with the remote interface.
|
39
|
+
*
|
40
|
+
* Initialize with with an object with a remotes property that is an Array of objects suitable
|
41
|
+
* for initializing remote objects and an optional active property that contains the index,
|
42
|
+
* in the remotes Array, of the active remote.
|
43
|
+
*/
|
44
|
+
RUKU.createRemoteManager = function(data) {
|
45
|
+
var processed = {};
|
46
|
+
|
47
|
+
/**
|
48
|
+
* Loads data into the remoteManager from an object as described in the createRemoteManager
|
49
|
+
* top level docs.
|
50
|
+
*/
|
51
|
+
function loadData(remoteData) {
|
52
|
+
this.remotes = [];
|
53
|
+
this.activeIndex = 0;
|
54
|
+
if (remoteData) {
|
55
|
+
for (var i = 0; i < remoteData.remotes.length; i++) {
|
56
|
+
this.remotes.push(RUKU.createRemote(remoteData.remotes[i]));
|
57
|
+
}
|
58
|
+
this.activeIndex = remoteData.active || 0;
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
loadData.call(processed, data);
|
63
|
+
|
64
|
+
/**
|
65
|
+
* Loads the remoteManager with data from the server.
|
66
|
+
*/
|
67
|
+
function load(callback) {
|
68
|
+
var that = this;
|
69
|
+
$.ajax({url:"/ajax", dataType:"json", data:{action:"list"}, success:function(data) {
|
70
|
+
that.loadData(data);
|
71
|
+
if (callback) {
|
72
|
+
callback();
|
73
|
+
}
|
74
|
+
}});
|
75
|
+
}
|
76
|
+
|
77
|
+
/**
|
78
|
+
* Sends the data contained in the remoteManager back to the server for saving.
|
79
|
+
*/
|
80
|
+
function save(callback) {
|
81
|
+
// Just hack together the JSON this one time...
|
82
|
+
var json = "{\"remotes\":[";
|
83
|
+
for (var i = 0; i < this.remotes.length; i++) {
|
84
|
+
var remote = this.remotes[i];
|
85
|
+
json = json + "{\"host\":\"" + remote.host + "\",\"name\":\"" + remote.name +
|
86
|
+
"\",\"port\":\"" + remote.port + "\"}";
|
87
|
+
if (i !== (this.remotes.length - 1)) {
|
88
|
+
json = json + ",";
|
89
|
+
}
|
90
|
+
}
|
91
|
+
json = json + "],\"active\":" + this.activeIndex + "}";
|
92
|
+
|
93
|
+
$.ajax({url:"/ajax", data:{action:"update", data:json}, success:function(resp) {
|
94
|
+
console.log(resp);
|
95
|
+
if (callback) {
|
96
|
+
callback();
|
97
|
+
}
|
98
|
+
}});
|
99
|
+
}
|
100
|
+
|
101
|
+
/**
|
102
|
+
* Returns the active remote or null if there are no remotes.
|
103
|
+
*/
|
104
|
+
function getActive() {
|
105
|
+
var activeRemote = null;
|
106
|
+
if (this.remotes.length > 0 && this.activeIndex < this.remotes.length) {
|
107
|
+
activeRemote = this.remotes[this.activeIndex];
|
108
|
+
}
|
109
|
+
return activeRemote;
|
110
|
+
}
|
111
|
+
|
112
|
+
/**
|
113
|
+
* Makes the given remote the active remote. Also accepts a String that is the new active remote's
|
114
|
+
* IP/hostname. If the remoteManager does not know about a remote with the given hostname then this
|
115
|
+
* is ignored.
|
116
|
+
*/
|
117
|
+
function setActive(newActive) {
|
118
|
+
var that = this;
|
119
|
+
var newHost = (newActive && newActive.host) ?
|
120
|
+
newActive.host : (typeof newActive === 'string') ? newActive : null;
|
121
|
+
if (newHost) {
|
122
|
+
$.each(this.remotes, function(index, remote) {
|
123
|
+
if (remote.host === newHost) {
|
124
|
+
that.activeIndex = index;
|
125
|
+
that.save();
|
126
|
+
return false;
|
127
|
+
}
|
128
|
+
});
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
function scanForFirst(callback) {
|
133
|
+
var that = this;
|
134
|
+
$.ajax({url:"/ajax", dataType:"json", data:{action:"scanForFirst"}, success:function(data) {
|
135
|
+
that.loadData(data);
|
136
|
+
if (callback) {
|
137
|
+
callback();
|
138
|
+
}
|
139
|
+
}});
|
140
|
+
}
|
141
|
+
|
142
|
+
function scanForAll(callback) {
|
143
|
+
var that = this;
|
144
|
+
$.ajax({url:"/ajax", dataType:"json", data:{action:"scanForAll"}, success:function(data) {
|
145
|
+
that.loadData(data);
|
146
|
+
if (callback) {
|
147
|
+
callback();
|
148
|
+
}
|
149
|
+
}});
|
150
|
+
}
|
151
|
+
|
152
|
+
return {
|
153
|
+
remotes: processed.remotes,
|
154
|
+
activeIndex: processed.activeIndex,
|
155
|
+
loadData:loadData,
|
156
|
+
getActive:getActive,
|
157
|
+
setActive:setActive,
|
158
|
+
load:load,
|
159
|
+
save:save,
|
160
|
+
scanForFirst:scanForFirst,
|
161
|
+
scanForAll:scanForAll
|
162
|
+
};
|
163
|
+
};
|
164
|
+
|
165
|
+
/**
|
166
|
+
* Creates an interface for managing remotes. The first parameter is a (jQuery wrapped) element
|
167
|
+
* that holds the main interface components. The second is an element containing the title for
|
168
|
+
* the active remote that needs to be updated when the active remote changes. The third parameter
|
169
|
+
* is the remoteManager to use for remote related operations.
|
170
|
+
*/
|
171
|
+
RUKU.createRemoteMenu = function(remoteMenu, activeRemoteTitle, remoteManagerToUse) {
|
172
|
+
var remoteManager = remoteManagerToUse || RUKU.createRemoteManager();
|
173
|
+
var container = remoteMenu.parent();
|
174
|
+
var remoteList = remoteMenu.find("#remoteList");
|
175
|
+
var firstBoxInfo = container.find("#firstBoxInfo");
|
176
|
+
|
177
|
+
/** Set the remoteManager for the menu to use for its remote operations */
|
178
|
+
function setRemoteManager(rm) {
|
179
|
+
remoteManager = rm;
|
180
|
+
}
|
181
|
+
|
182
|
+
/** Get the remote that is active according to the remoteManager */
|
183
|
+
function getActiveRemote() {
|
184
|
+
return remoteManager.getActive();
|
185
|
+
}
|
186
|
+
|
187
|
+
/** Returns true if there is at least one remote managed by the remoteManager */
|
188
|
+
function hasRemotes() {
|
189
|
+
return remoteManager &&
|
190
|
+
remoteManager.remotes &&
|
191
|
+
remoteManager.remotes.length > 0;
|
192
|
+
}
|
193
|
+
|
194
|
+
/** Updates the text of the activeRemoteTitle element */
|
195
|
+
function updateActiveRemoteTitle() {
|
196
|
+
var activeRemote;
|
197
|
+
if (remoteManager) {
|
198
|
+
activeRemote = remoteManager.getActive();
|
199
|
+
}
|
200
|
+
if (activeRemote) {
|
201
|
+
var name = ($.trim(activeRemote.name)) === "" ? "(Box with no name)" : activeRemote.name;
|
202
|
+
activeRemoteTitle.text(name);
|
203
|
+
var altText = name + " (" + activeRemote.host + ")";
|
204
|
+
activeRemoteTitle.attr("alt", altText);
|
205
|
+
activeRemoteTitle.attr("title", altText);
|
206
|
+
} else {
|
207
|
+
var text = "(Must set up remote)";
|
208
|
+
activeRemoteTitle.text(text);
|
209
|
+
activeRemoteTitle.attr("alt", text);
|
210
|
+
activeRemoteTitle.attr("title", text);
|
211
|
+
}
|
212
|
+
}
|
213
|
+
|
214
|
+
/** Scan for all remotes on the network and show the results */
|
215
|
+
function scanForAll() {
|
216
|
+
remoteManager.scanForAll(function() {
|
217
|
+
updateActiveRemoteTitle();
|
218
|
+
show();
|
219
|
+
});
|
220
|
+
}
|
221
|
+
|
222
|
+
/**
|
223
|
+
* Scan for the first remote found on the network (the common case is one remote)
|
224
|
+
* and show display the result
|
225
|
+
*/
|
226
|
+
function scanForFirst() {
|
227
|
+
remoteManager.scanForFirst(function() {
|
228
|
+
updateActiveRemoteTitle();
|
229
|
+
remoteList.css("display", "none");
|
230
|
+
firstBoxInfo.empty();
|
231
|
+
if (hasRemotes()) {
|
232
|
+
firstBoxInfo
|
233
|
+
.append($("<div>").addClass("message").text("One box found"))
|
234
|
+
.append($("<div>").addClass("boxInfo")
|
235
|
+
.append($("<img>").attr("src", "/images/box-medium.png"))
|
236
|
+
.append(
|
237
|
+
$("<div>").addClass("details")
|
238
|
+
.append($("<div>").addClass("name").text(getActiveRemote().name))
|
239
|
+
.append($("<div>").addClass("host").text(getActiveRemote().host))
|
240
|
+
)
|
241
|
+
)
|
242
|
+
.append(
|
243
|
+
$("<div>").addClass("menu")
|
244
|
+
.append($("<a>").addClass("buttonLink").addClass("doneButton")
|
245
|
+
.text("this is my only box").attr("href", "#")
|
246
|
+
.click(function() {
|
247
|
+
hide();
|
248
|
+
return false;
|
249
|
+
}))
|
250
|
+
.append($("<a>").addClass("buttonLink").addClass("moreButton")
|
251
|
+
.text("scan for more boxes").attr("href", "#")
|
252
|
+
.click(function() {
|
253
|
+
scanForAll();
|
254
|
+
return false;
|
255
|
+
}))
|
256
|
+
);
|
257
|
+
} else {
|
258
|
+
firstBoxInfo.text("Didn't find anything");
|
259
|
+
}
|
260
|
+
firstBoxInfo.css("display", "block");
|
261
|
+
});
|
262
|
+
}
|
263
|
+
|
264
|
+
function addRemote(host, name) {
|
265
|
+
if (host === "") {
|
266
|
+
return;
|
267
|
+
}
|
268
|
+
if (!name) {
|
269
|
+
if (remoteManager.remotes.length === 0) {
|
270
|
+
name = "My Roku Box";
|
271
|
+
} else {
|
272
|
+
name = host;
|
273
|
+
}
|
274
|
+
}
|
275
|
+
remoteManager.remotes.push({host:host, name:name});
|
276
|
+
remoteManager.save(function() {
|
277
|
+
show();
|
278
|
+
});
|
279
|
+
}
|
280
|
+
|
281
|
+
/**
|
282
|
+
* Updates the name of the remote with the given hostname and causes the remoteManager to save
|
283
|
+
* the data to the server if the name has changed.
|
284
|
+
*/
|
285
|
+
function updateRemoteName(remoteHost, remoteName) {
|
286
|
+
var changed = false;
|
287
|
+
|
288
|
+
for (var i = 0; i < remoteManager.remotes.length; i++) {
|
289
|
+
var remote = remoteManager.remotes[i];
|
290
|
+
if (remote.host === remoteHost) {
|
291
|
+
if (remote.name !== remoteName) {
|
292
|
+
remote.name = remoteName;
|
293
|
+
changed = true;
|
294
|
+
}
|
295
|
+
break;
|
296
|
+
}
|
297
|
+
}
|
298
|
+
|
299
|
+
if (changed) {
|
300
|
+
remoteManager.save();
|
301
|
+
updateActiveRemoteTitle();
|
302
|
+
}
|
303
|
+
}
|
304
|
+
|
305
|
+
/**
|
306
|
+
* Changes the active remote to the one with the given hostname. If there are no remotes with the
|
307
|
+
* given hostname then there is no effect.
|
308
|
+
*/
|
309
|
+
function changeActiveRemote (newActiveRemoteHost) {
|
310
|
+
$.each(remoteManager.remotes, function(index, remote) {
|
311
|
+
if (remote.host === newActiveRemoteHost) {
|
312
|
+
remoteManager.setActive(remote);
|
313
|
+
updateActiveRemoteTitle();
|
314
|
+
return false;
|
315
|
+
}
|
316
|
+
});
|
317
|
+
}
|
318
|
+
|
319
|
+
/** Render a list of available remotes */
|
320
|
+
function show() {
|
321
|
+
remoteList.empty();
|
322
|
+
if (hasRemotes()) {
|
323
|
+
for (var i=0; i < remoteManager.remotes.length; i++) {
|
324
|
+
var remote = remoteManager.remotes[i];
|
325
|
+
var boxDiv = $("<div>").addClass("remote")
|
326
|
+
.append($("<div>")
|
327
|
+
.append($("<input>").addClass("activeRemoteRadio").attr("type", "radio")
|
328
|
+
.attr("name", "activeRemote").val(remote.host))
|
329
|
+
.change(function(host) {
|
330
|
+
return function() {
|
331
|
+
changeActiveRemote(host);
|
332
|
+
};
|
333
|
+
}(remote.host)
|
334
|
+
)
|
335
|
+
)
|
336
|
+
.append($("<div>").append($("<img>").addClass("boxPic").attr("src", "/images/box-small.png")))
|
337
|
+
.append($("<div>").addClass("info")
|
338
|
+
.append($("<input>").addClass("name")
|
339
|
+
.attr("type", "text").val(remote.name || "(no name)")
|
340
|
+
// Add the ability to edit the name of a remote by clicking on it and typing
|
341
|
+
.hover(function() { $(this).addClass("over"); }, function() { $(this).removeClass("over"); })
|
342
|
+
.blur(function(host) {
|
343
|
+
return function() {
|
344
|
+
updateRemoteName(host, $(this).val());
|
345
|
+
};
|
346
|
+
}(remote.host))
|
347
|
+
.keypress(function() {
|
348
|
+
// Make the Return key complete the name edit
|
349
|
+
if (event.which == '13') {
|
350
|
+
event.preventDefault();
|
351
|
+
$(this).blur();
|
352
|
+
$(this).removeClass("over");
|
353
|
+
}
|
354
|
+
}))
|
355
|
+
.append($("<div>").addClass("host").text(remote.host))
|
356
|
+
);
|
357
|
+
|
358
|
+
if (i === remoteManager.activeIndex) {
|
359
|
+
boxDiv.addClass("active");
|
360
|
+
boxDiv.find(".activeRemoteRadio").attr("checked", true);
|
361
|
+
}
|
362
|
+
remoteList.append(boxDiv);
|
363
|
+
}
|
364
|
+
} else {
|
365
|
+
remoteList.append(
|
366
|
+
$("<div>").addClass("noRemotesMessage")
|
367
|
+
.html(
|
368
|
+
"No remotes have been set up yet. Click the <b>scan for boxes</b> button above " +
|
369
|
+
"to find remotes on your network. If you know the IP address or hostname of your " +
|
370
|
+
"box you can add it below instead."
|
371
|
+
)
|
372
|
+
);
|
373
|
+
}
|
374
|
+
|
375
|
+
remoteList.append(
|
376
|
+
$("<div>").attr("id", "manualAddBox")
|
377
|
+
.append("Add a box by IP: ")
|
378
|
+
.append($("<input>").attr("type", "text").attr("id", "manualAddBoxInput"))
|
379
|
+
.append($("<a>").addClass("buttonLink").text("Add")
|
380
|
+
.click(function() {
|
381
|
+
addRemote($("#manualAddBoxInput").val());
|
382
|
+
})
|
383
|
+
)
|
384
|
+
);
|
385
|
+
|
386
|
+
container.css("display", "block");
|
387
|
+
remoteList.css("display", "block");
|
388
|
+
firstBoxInfo.css("display", "none");
|
389
|
+
remoteMenu.fadeIn();
|
390
|
+
}
|
391
|
+
|
392
|
+
/**
|
393
|
+
* Loads remote data from the server. If no remotes are found this displays
|
394
|
+
* a 'no remotes' message to the user.
|
395
|
+
*/
|
396
|
+
function load() {
|
397
|
+
remoteManager.load(function() {
|
398
|
+
updateActiveRemoteTitle();
|
399
|
+
if (!hasRemotes()) {
|
400
|
+
// Since we cannot do anything without remotes, go ahead and call the show() method which
|
401
|
+
// will display the 'no remotes' message in this case.
|
402
|
+
show();
|
403
|
+
}
|
404
|
+
});
|
405
|
+
}
|
406
|
+
|
407
|
+
/** Hide the menu */
|
408
|
+
function hide() {
|
409
|
+
remoteMenu.fadeOut("fast", function() {
|
410
|
+
container.css("display", "none");
|
411
|
+
});
|
412
|
+
}
|
413
|
+
|
414
|
+
function listRemotes() {
|
415
|
+
if (hasRemotes()) {
|
416
|
+
// We already have some remotes. Just display them.
|
417
|
+
show();
|
418
|
+
} else {
|
419
|
+
remoteManager.load(function() {
|
420
|
+
updateActiveRemoteTitle();
|
421
|
+
show();
|
422
|
+
});
|
423
|
+
}
|
424
|
+
}
|
425
|
+
|
426
|
+
remoteMenu.find("#closeButton").click(function() {
|
427
|
+
hide();
|
428
|
+
return false;
|
429
|
+
});
|
430
|
+
|
431
|
+
remoteMenu.find("#scanButton").click(function() {
|
432
|
+
scanForFirst();
|
433
|
+
return false;
|
434
|
+
});
|
435
|
+
|
436
|
+
return {
|
437
|
+
remoteManager:remoteManager,
|
438
|
+
setRemoteManager:setRemoteManager,
|
439
|
+
getActiveRemote:getActiveRemote,
|
440
|
+
hasRemotes:hasRemotes,
|
441
|
+
load:load,
|
442
|
+
show:show,
|
443
|
+
hide:hide,
|
444
|
+
listRemotes:listRemotes,
|
445
|
+
updateActiveRemoteTitle:updateActiveRemoteTitle
|
446
|
+
};
|
447
|
+
};
|
data/lib/ruku/remote.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Ruku
|
5
|
+
|
6
|
+
# Class for Remote to extend that removes most instance methods
|
7
|
+
class BlankSlate
|
8
|
+
# Keep a few basics and some YAML related methods
|
9
|
+
KEEPERS = %w[class object_id != inspect to_yaml to_yaml_style to_yaml_properties] +
|
10
|
+
%w[taguri instance_variables instance_variable_get]
|
11
|
+
|
12
|
+
instance_methods.each do |m|
|
13
|
+
# Whack every method except those that start with __ or end with ?, figuring
|
14
|
+
# that those would be unlikely to be tried to use for Roku commands and
|
15
|
+
# possibly useful for other basic stuff.
|
16
|
+
ms = m.to_s
|
17
|
+
undef_method m unless (ms =~ /^__/ || ms =~ /\?$/ || KEEPERS.include?(ms))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Communicates with a Roku box. Known methods that you can call that correspond with buttons
|
22
|
+
# on the physical Roku remote include: up, down, left, right, select, home, fwd, back, pause
|
23
|
+
#
|
24
|
+
# Calling methods with the name of the command you want to send has the same effect as calling
|
25
|
+
# send_roku_command with the Symbol (or String) representing your command.
|
26
|
+
#
|
27
|
+
# Examples:
|
28
|
+
#
|
29
|
+
# remote = Remote.new('192.168.1.10') # Use your Roku box IP or hostname
|
30
|
+
# remote.pause # Sends play/pause to the Roku box
|
31
|
+
# remote.left.down.select # Can chain commands if you want to
|
32
|
+
#
|
33
|
+
# Actually, all prefixes will work for all commands because they are accepted by the Roku box.
|
34
|
+
# Unless more commands are added to introduce ambiguities, this means only one character is
|
35
|
+
# needed for a command.
|
36
|
+
#
|
37
|
+
# Examples:
|
38
|
+
#
|
39
|
+
# remote.h # Registers as 'home'
|
40
|
+
#
|
41
|
+
# remote.d # This and the following 3 lines register as 'down'
|
42
|
+
# remote.do
|
43
|
+
# remote.dow
|
44
|
+
# remote.down
|
45
|
+
#
|
46
|
+
# Despite this working now it is not recommended that you use this in case future added commands
|
47
|
+
# create ambiguity and also because it can make code less clear.
|
48
|
+
class Remote < BlankSlate
|
49
|
+
# The port on which the Roku box listens for commands
|
50
|
+
DEFAULT_PORT = 8080
|
51
|
+
|
52
|
+
# Known commands that the Roku box will accept - here mostly for documentation purposes
|
53
|
+
KNOWN_COMMANDS = %w[up down left right select home fwd back pause]
|
54
|
+
|
55
|
+
# Scan for Roku boxes on the local network
|
56
|
+
def self.scan(stop_on_first=false)
|
57
|
+
# TODO: don't just use the typical IP/subnet for a home network; figure it out
|
58
|
+
boxes = []
|
59
|
+
prefix = '192.168.1.'
|
60
|
+
(0..255).each do |host|
|
61
|
+
info = Socket.getaddrinfo(prefix + host.to_s, DEFAULT_PORT)
|
62
|
+
# Is there a better way to identify a Roku box other than looking for a hostname
|
63
|
+
# starting with 'NP-'? There probably is - is the better way also quick?
|
64
|
+
boxes << Remote.new(info[0][3]) if info[0][2] =~ /^NP-/i
|
65
|
+
break if stop_on_first && !boxes.empty?
|
66
|
+
end
|
67
|
+
boxes
|
68
|
+
end
|
69
|
+
|
70
|
+
attr_accessor :host, :name, :port
|
71
|
+
|
72
|
+
def initialize(host, name=nil, port=DEFAULT_PORT)
|
73
|
+
@host, @name, @port = host, name, port
|
74
|
+
end
|
75
|
+
|
76
|
+
# Send a "raw" command to the Roku player that is not formatted in the typical style that
|
77
|
+
# the box is expecting to receive.
|
78
|
+
def send_command(cmd)
|
79
|
+
use_tcp_socket {|s| s.write cmd}
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
# Send a command to the Roku box in the form "press CMD\n" that is used for known commands.
|
84
|
+
def send_roku_command(cmd)
|
85
|
+
cmd_string = cmd.to_s # In case it's a symbol, basically
|
86
|
+
|
87
|
+
# For some reason the Roku box can be unresponsive with the full 'select' command. It seems
|
88
|
+
# that 'sel' will always work, though, so send that.
|
89
|
+
cmd_string = 'sel' if cmd_string == 'select'
|
90
|
+
|
91
|
+
send_command "press #{cmd_string}\n"
|
92
|
+
end
|
93
|
+
|
94
|
+
# Consider missing methods to be names of commands to send to the box
|
95
|
+
def method_missing(*args)
|
96
|
+
# If more than one argument, assume someone is trying to use a method
|
97
|
+
# that they don't expect will be turned into a Roku command
|
98
|
+
throw 'Roku command takes no arguments' if args.size > 1
|
99
|
+
roku_cmd = args[0]
|
100
|
+
send_roku_command roku_cmd
|
101
|
+
end
|
102
|
+
|
103
|
+
# Overide normal Object select to make it work in the Roku remote sense
|
104
|
+
def select
|
105
|
+
send_roku_command :select
|
106
|
+
end
|
107
|
+
|
108
|
+
# Remotes with the same host are considered equal
|
109
|
+
def ==(other)
|
110
|
+
host == other.host
|
111
|
+
end
|
112
|
+
|
113
|
+
def eql?(other)
|
114
|
+
self == other
|
115
|
+
end
|
116
|
+
|
117
|
+
def hash
|
118
|
+
host.hash
|
119
|
+
end
|
120
|
+
|
121
|
+
def <=>(other)
|
122
|
+
host <=> other.host
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_s
|
126
|
+
"<Remote host:#{@host}, name:#{@name || '(none)'}>"
|
127
|
+
end
|
128
|
+
|
129
|
+
protected
|
130
|
+
|
131
|
+
# Do something with the TCPSocket in the given block. The socket will be closed afterward.
|
132
|
+
def use_tcp_socket
|
133
|
+
s = TCPSocket.open(@host, @port)
|
134
|
+
yield s
|
135
|
+
s.close
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
data/lib/ruku/remotes.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
module Ruku
|
2
|
+
# A collection of Ruku::Remotes. Keeps track of one or multiple Remotes for Roku
|
3
|
+
# boxes. Manages things like which box is active for use.
|
4
|
+
class Remotes
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_reader :active_index
|
8
|
+
attr_accessor :boxes, :storage
|
9
|
+
|
10
|
+
def initialize(boxes=[], storage=SimpleStorage.new)
|
11
|
+
@boxes, @storage = boxes, storage
|
12
|
+
boxes.uniq!
|
13
|
+
@active_index = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def each(&b)
|
17
|
+
boxes.each &b
|
18
|
+
end
|
19
|
+
|
20
|
+
def size
|
21
|
+
boxes.size
|
22
|
+
end
|
23
|
+
|
24
|
+
def empty?
|
25
|
+
boxes.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
def [](index)
|
29
|
+
boxes[index]
|
30
|
+
end
|
31
|
+
|
32
|
+
def []=(index, box)
|
33
|
+
boxes[index] = box
|
34
|
+
end
|
35
|
+
|
36
|
+
def active
|
37
|
+
return boxes[@active_index] if !boxes.empty? && boxes.size > active_index
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_active(box)
|
42
|
+
new_index = nil
|
43
|
+
boxes.each_with_index {|b,i| new_index = i if b == box} if box.is_a? Remote
|
44
|
+
self.active_index = new_index if new_index and new_index < boxes.size
|
45
|
+
end
|
46
|
+
|
47
|
+
def find_by_host(host)
|
48
|
+
boxes.find {|box| box.host == host}
|
49
|
+
end
|
50
|
+
|
51
|
+
def add(box)
|
52
|
+
boxes << box
|
53
|
+
boxes.sort!.uniq!
|
54
|
+
end
|
55
|
+
|
56
|
+
def remove(box)
|
57
|
+
host = box.is_a?(Remote) ? box.host : box
|
58
|
+
boxes.reject! { |b| b.host == host }
|
59
|
+
self.active_index = 0 if @active_index >= boxes.size
|
60
|
+
boxes.sort!
|
61
|
+
end
|
62
|
+
|
63
|
+
def store
|
64
|
+
storage.store(self)
|
65
|
+
end
|
66
|
+
|
67
|
+
def load
|
68
|
+
loaded = storage.load
|
69
|
+
self.boxes, self.active_index = loaded.boxes, loaded.active_index
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def active_index=(index)
|
75
|
+
@active_index = index
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|