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.
Files changed (55) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +96 -0
  3. data/Rakefile +33 -0
  4. data/bin/ruku +50 -0
  5. data/lib/ruku/clients/simple.rb +232 -0
  6. data/lib/ruku/clients/tk.rb +36 -0
  7. data/lib/ruku/clients/web.rb +117 -0
  8. data/lib/ruku/clients/web_static/css/ruku.css +196 -0
  9. data/lib/ruku/clients/web_static/images/box-medium.png +0 -0
  10. data/lib/ruku/clients/web_static/images/box-small.png +0 -0
  11. data/lib/ruku/clients/web_static/images/remote/back-over.png +0 -0
  12. data/lib/ruku/clients/web_static/images/remote/back.png +0 -0
  13. data/lib/ruku/clients/web_static/images/remote/down-over.png +0 -0
  14. data/lib/ruku/clients/web_static/images/remote/down.png +0 -0
  15. data/lib/ruku/clients/web_static/images/remote/fwd-over.png +0 -0
  16. data/lib/ruku/clients/web_static/images/remote/fwd.png +0 -0
  17. data/lib/ruku/clients/web_static/images/remote/home-over.png +0 -0
  18. data/lib/ruku/clients/web_static/images/remote/home.png +0 -0
  19. data/lib/ruku/clients/web_static/images/remote/left-over.png +0 -0
  20. data/lib/ruku/clients/web_static/images/remote/left.png +0 -0
  21. data/lib/ruku/clients/web_static/images/remote/pause-over.png +0 -0
  22. data/lib/ruku/clients/web_static/images/remote/pause.png +0 -0
  23. data/lib/ruku/clients/web_static/images/remote/right-over.png +0 -0
  24. data/lib/ruku/clients/web_static/images/remote/right.png +0 -0
  25. data/lib/ruku/clients/web_static/images/remote/select-over.png +0 -0
  26. data/lib/ruku/clients/web_static/images/remote/select.png +0 -0
  27. data/lib/ruku/clients/web_static/images/remote/space1.png +0 -0
  28. data/lib/ruku/clients/web_static/images/remote/space2.png +0 -0
  29. data/lib/ruku/clients/web_static/images/remote/space3.png +0 -0
  30. data/lib/ruku/clients/web_static/images/remote/up-over.png +0 -0
  31. data/lib/ruku/clients/web_static/images/remote/up.png +0 -0
  32. data/lib/ruku/clients/web_static/images/spacer.gif +0 -0
  33. data/lib/ruku/clients/web_static/index.html +203 -0
  34. data/lib/ruku/clients/web_static/js/jquery-1.4.2.js +154 -0
  35. data/lib/ruku/clients/web_static/js/ruku.js +447 -0
  36. data/lib/ruku/remote.rb +138 -0
  37. data/lib/ruku/remotes.rb +78 -0
  38. data/lib/ruku/storage.rb +77 -0
  39. data/lib/ruku.rb +5 -0
  40. data/ruku.gemspec +31 -0
  41. data/test/helper.rb +11 -0
  42. data/test/js/qunit.css +119 -0
  43. data/test/js/qunit.js +1069 -0
  44. data/test/js/runner.html +29 -0
  45. data/test/js/test_remote.js +37 -0
  46. data/test/js/test_remote_manager.js +186 -0
  47. data/test/js/test_remote_menu.js +208 -0
  48. data/test/js/test_util.js +15 -0
  49. data/test/test_remote.rb +89 -0
  50. data/test/test_remotes.rb +144 -0
  51. data/test/test_simple_client.rb +166 -0
  52. data/test/test_simple_storage.rb +70 -0
  53. data/test/test_web_client.rb +46 -0
  54. data/test/test_yaml_storage.rb +54 -0
  55. 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
+ };
@@ -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
@@ -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