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
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Aaron Royer
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,96 @@
1
+ = Ruku -- Roku™ set-top box remote control, command line and web interfaces
2
+
3
+ == Installation
4
+
5
+ gem install ruku
6
+
7
+ == Usage
8
+
9
+ The following launches the web interface. See it at http://localhost:3030
10
+
11
+ ruku --web
12
+
13
+ You can use this to scan for or add boxes and start controlling them. I recommend
14
+ using the keyboard (super snappy controlling!). Arrow keys (and vi directional
15
+ keys) work, Space plays and pauses, Enter selects, and Esc is home.
16
+
17
+ You can also just use the command line. Ruku needs to know about your Roku
18
+ box(es). If you haven't added any boxes with the web interface, try:
19
+
20
+ ruku scan
21
+
22
+ This will try to scan your network to find boxes. Read on below if it didn't.
23
+ Assuming you have a least one box set up, you can start sending commands.
24
+
25
+ ruku pause # Play/pause
26
+ ruku left
27
+ ruku up
28
+ ruku select
29
+ ruku fwd # Fast forward
30
+ ruku back # Rewind
31
+
32
+ Known commands are: up down left right select home fwd back pause
33
+
34
+ Making an alias for 'ruku pause' is nice for quick pause/play while computing.
35
+
36
+ If scanning doesn't work for adding boxes then you'll have to figure out what
37
+ the IP of the box is - it's available in Settings -> Player Info on the box.
38
+ Then add it manually:
39
+
40
+ ruku add IP
41
+
42
+ Any method of adding boxes creates a '.ruku-boxes' file in your $HOME directory
43
+ that contains an IP or hostname per line (followed optionally by a colon and a
44
+ nickname for that box). You can edit or create this yourself. You can see all
45
+ known boxes with:
46
+
47
+ ruku list
48
+
49
+ For more help:
50
+
51
+ ruku --help
52
+
53
+ == Development
54
+
55
+ === Source Repository
56
+
57
+ http://github.com/aaronroyer/ruku
58
+
59
+ Git clone URL is
60
+
61
+ * git://github.com/aaronroyer/ruku.git
62
+
63
+ === Issues and Bug Reports
64
+
65
+ You can open issues at Github
66
+
67
+ * http://github.com/aaronroyer/ruku/issues
68
+
69
+ Or you can send me an email: aaronroyer@gmail.com
70
+
71
+ == Legal/Disclaimer
72
+
73
+ Roku and the Roku logo are trademarks of Roku Inc. in the United States and other countries.
74
+
75
+ Ruku is not made, supported, or endorsed by Roku Inc.
76
+
77
+ == License
78
+
79
+ Ruku is MIT licensed.
80
+
81
+ :include: MIT-LICENSE
82
+
83
+ = Other stuff
84
+
85
+ Author:: Aaron Royer <aaronroyer@gmail.com>
86
+ Requires:: Ruby 1.8.6 or later
87
+ License:: Copyright 2010 by Aaron Royer.
88
+ Released under an MIT-style license. See the LICENSE file
89
+ included in the distribution.
90
+
91
+ == Warranty
92
+
93
+ This software is provided "as is" and without any express or
94
+ implied warranties, including, without limitation, the implied
95
+ warranties of merchantability and fitness for a particular
96
+ purpose.
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'lib'
7
+ t.pattern = 'test/**/test_*.rb'
8
+ end
9
+
10
+ Rake::RDocTask.new do |rdoc|
11
+ rdoc.rdoc_dir = 'rdoc'
12
+ rdoc.title = 'ruku'
13
+ rdoc.options << '--line-numbers' << '--inline-source'
14
+ rdoc.rdoc_files.include('README*')
15
+ rdoc.rdoc_files.include('lib/**/*.rb')
16
+ end
17
+
18
+ begin
19
+ require 'rcov/rcovtask'
20
+ Rcov::RcovTask.new do |t|
21
+ t.libs << 'test'
22
+ t.test_files = FileList['test/**/test_*.rb']
23
+ t.verbose = true
24
+ end
25
+ rescue LoadError
26
+ end
27
+
28
+ desc "Build gem package"
29
+ task :package => 'ruku.gemspec' do
30
+ sh "gem build ruku.gemspec"
31
+ end
32
+
33
+ task :default => [:test]
data/bin/ruku ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+ begin
3
+ require 'ruku/clients/simple'
4
+ require 'ruku/clients/web'
5
+ rescue LoadError
6
+ # For testing and dev and stuff
7
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
8
+ require 'ruku/clients/simple'
9
+ require 'ruku/clients/web'
10
+ end
11
+
12
+ if ARGV.empty? || ARGV.include?('-h') || ARGV.include?('--help')
13
+ help = <<HELP
14
+ Usage: ruku COMMAND/OPERATION
15
+
16
+ Controller for Roku set top boxes. You will need to first scan the network for
17
+ boxes using 'ruku scan' or add boxes manually using 'ruku add HOST'. Once
18
+ 'ruku list' shows an active box, you can start sending commands like
19
+ 'ruku pause' (play/pause) or 'ruku left' (Roku remote left button).
20
+
21
+ COMMAND is a command to be sent to the active Roku box, if there is one.
22
+ Known Roku commands:
23
+ #{Ruku::Remote::KNOWN_COMMANDS.join(', ')}
24
+ (you can send unknown commands; the box should ignore)
25
+ The -c is available in case you need to explicitly send a command to the
26
+ Roku box, to disambiguate from a ruku operation (see below)
27
+
28
+ OPERATION is for managing the Roku boxes for ruku to use.
29
+ ruku operations:
30
+ scan Scan for Roku boxes on the network
31
+ list List Roku boxes
32
+ name NUM NAME Set the name of a box from the list
33
+ add HOST_OR_IP NAME Add a box with the HOST_OR_IP and (optional) NAME
34
+
35
+ Alternatively, use 'ruku --web' to start the server for the web client. You
36
+ should then be able to visit http://localhost:3030 to see the interface. You
37
+ can specify a different port with '-p PORT'.
38
+
39
+ Options:
40
+ -c, --force-roku-command Force send command to the active Roku box
41
+ --web Fire up the web client (port 3030 default)
42
+ -p, --port PORT Specify port (only use with --web)
43
+ -h, --help Show this message
44
+ HELP
45
+ puts help
46
+ elsif ARGV.include?('--web')
47
+ Ruku::Clients::Web.new.start
48
+ else
49
+ Ruku::Clients::Simple.new.run_from_command_line
50
+ end
@@ -0,0 +1,232 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. ruku])
2
+ require 'optparse'
3
+ require 'ostruct'
4
+
5
+ module Ruku
6
+ module Clients
7
+ # Provides a little wrapper around a Ruku::Remotes for ease of making
8
+ # a command line client or messing around in an IRB session
9
+ class Simple
10
+ OPERATION_NAMES = %w[scan list add remove name activate help]
11
+
12
+ attr_accessor :remotes
13
+
14
+ def initialize(rs=Ruku::Remotes.new)
15
+ @remotes = rs
16
+ end
17
+
18
+ # Run from the command line. This parses options as well as the command
19
+ # or Ruku operation to run.
20
+ def run_from_command_line
21
+ handle_exceptions do
22
+ remotes.load
23
+ handle_options
24
+ execute_command_from_command_line
25
+ end
26
+ end
27
+
28
+ # Checks the command line arguments for a command (options should have already
29
+ # been parsed and removed) and then sends a Roku command or runs an operation
30
+ # on the RemoteManager.
31
+ def execute_command_from_command_line
32
+ cmd = ARGV[0]
33
+ if not cmd
34
+ puts CMD_LINE_HELP
35
+ elsif OPERATION_NAMES.include?(cmd) && !options.force_command
36
+ begin
37
+ self.send(*ARGV)
38
+ rescue ArgumentError => ex
39
+ $stderr.puts "Wrong number of arguments (#{ARGV.size-1}) for operation: #{cmd}"
40
+ end
41
+ else
42
+ send_roku_command cmd
43
+ end
44
+ end
45
+
46
+ # Client options generally parsed from the command line
47
+ def options
48
+ @options ||= OpenStruct.new
49
+ end
50
+
51
+ # Parse and handle command line options
52
+ def handle_options
53
+ opts = OptionParser.new do |opts|
54
+ opts.on('-c', '--force-roku-command') { options.force_command = true }
55
+ end.parse!
56
+ end
57
+
58
+ # Send a command to the active box
59
+ def send_roku_command(cmd)
60
+ if remotes.empty?
61
+ raise UsageError, "No known Roku boxes\n" +
62
+ "Try 'ruku scan' to find them, or 'ruku add HOST NAME' to add one manually"
63
+ else
64
+ remotes.active.send_roku_command cmd
65
+ end
66
+ end
67
+
68
+ # Ruku command line "operations" follow
69
+
70
+ # Scan for boxes
71
+ def scan
72
+ remotes.boxes = Ruku::Remote.scan
73
+ remotes.each_with_index do |box, i|
74
+ box.name = "My Roku Box#{i == 1 ? i+1 : ''}"
75
+ end
76
+ if remotes.empty?
77
+ puts 'Did not find any Roku boxes'
78
+ else
79
+ puts 'Roku boxes found:'
80
+ remotes.each_with_index do |box, i|
81
+ print "#{i+1}. #{box.name || '(no name)'} at #{box.host}"
82
+ print "#{' <-- active' if i == remotes.active_index && remotes.size > 1}\n"
83
+ end
84
+ store
85
+ end
86
+ end
87
+
88
+ # List the boxes we know about
89
+ def list
90
+ if remotes.empty?
91
+ puts "No Roku boxes known\n" +
92
+ "Use the scan or add operations to find or add boxes"
93
+ else
94
+ puts 'Roku boxes:'
95
+ remotes.each_with_index do |box, i|
96
+ print "#{i+1}. #{box.name || '(no name)'} at #{box.host}"
97
+ print "#{' <-- active' if i == remotes.active_index}\n"
98
+ end
99
+ end
100
+ end
101
+
102
+ # Add a box
103
+ def add(host=nil, name='My Roku Box')
104
+ raise UsageError, 'Must specify host of box to add' if not host
105
+
106
+ if existing = remotes.find_by_host(host)
107
+ existing.name = name
108
+ else
109
+ remotes.add(Ruku::Remote.new(host, name))
110
+ end
111
+ store
112
+ puts "Added remote with host: #{host} and name: #{name}"
113
+ end
114
+
115
+ # Remove a box with the given number (from the list operation) or hostname
116
+ def remove(number=nil)
117
+ raise UsageError, 'Must specify number from boxes list or hostname/IP address' if not number
118
+
119
+ prev_count = remotes.size
120
+ msg = 'Box '
121
+ if number.is_a?(Integer) || number =~ /^\d+$/
122
+ index = number.to_i - 1
123
+ remotes.boxes.delete_at(index)
124
+ msg << (index + 1).to_s
125
+ else
126
+ remotes.remove(number)
127
+ msg << "with IP/host #{number}"
128
+ end
129
+ msg << ' removed'
130
+
131
+ if prev_count == remotes.size + 1
132
+ remotes.store
133
+ puts msg
134
+ else
135
+ puts "Could not remove box: #{number}"
136
+ end
137
+ end
138
+
139
+ def name(number=nil, name=nil)
140
+ raise UsageError, 'Must specify number from remotes list or IP/hostname' if not number
141
+ raise UsageError, 'Must specify name for box' if not name
142
+
143
+ msg = 'Box '
144
+ if number.is_a?(Integer) || number =~ /^\d+$/
145
+ self[number.to_i].name = name
146
+ msg << (number).to_s
147
+ else
148
+ remotes.find_by_host(number).name = name
149
+ msg << "with IP/host #{number}"
150
+ end
151
+ msg << " renamed to #{name}"
152
+ store
153
+ puts msg
154
+ end
155
+
156
+ def activate(number=nil)
157
+ raise UsageError, 'Must specify number from remotes list or IP/hostname' if not number
158
+
159
+ msg = 'Box '
160
+ box = if number.is_a?(Integer) || number =~ /^\d+$/
161
+ msg << (number).to_s
162
+ self[number.to_i]
163
+ else
164
+ msg << "with IP/host #{number}"
165
+ remotes.find_by_host(number)
166
+ end
167
+
168
+ if box
169
+ remotes.set_active(box)
170
+ store
171
+ puts msg + ' activated for use'
172
+ else
173
+ puts 'Unknown box specified'
174
+ end
175
+ end
176
+
177
+ def help
178
+ puts CMD_LINE_HELP
179
+ end
180
+
181
+ # Methods not directly available from the command line
182
+
183
+ # Get remotes using 1-based index for the command line
184
+ def [](num)
185
+ remotes[num-1]
186
+ end
187
+
188
+ # Assign and store remotes using 1-based index for the command line
189
+ def []=(num, box)
190
+ remotes[num-1] = box
191
+ store
192
+ end
193
+
194
+ def store
195
+ remotes.store
196
+ end
197
+
198
+ private
199
+
200
+ def handle_exceptions
201
+ begin
202
+ yield
203
+ rescue SystemExit
204
+ exit
205
+ rescue UsageError => ex
206
+ $stderr.puts ex.message
207
+ exit 1
208
+ rescue OptionParser::InvalidOption => ex
209
+ $stderr.puts ex.message
210
+ exit 1
211
+ rescue Exception => ex
212
+ display_error_message ex
213
+ exit 1
214
+ end
215
+ end
216
+
217
+ def display_error_message(ex)
218
+ msg = <<MSG
219
+ The Ruku application has aborted! If this is unexpected, you may want to open
220
+ an issue at github.com/aaronroyer/ruku to get a possible bug fixed. If you do,
221
+ please include the debug information below.
222
+ MSG
223
+ $stderr.puts msg
224
+ $stderr.puts ex.message
225
+ $stderr.puts ex.backtrace
226
+ end
227
+ end
228
+
229
+ class UsageError < Exception
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,36 @@
1
+
2
+ module Ruku
3
+ class TkClient
4
+ def initialize
5
+ sm = YAMLStorage.new
6
+ sm.load
7
+ @remote = Remote.new(sm.boxes.first.host, 8080)
8
+
9
+ launch
10
+ end
11
+
12
+ def launch
13
+ require 'tk'
14
+ root = TkRoot.new() { title "Roku Remote"}
15
+ root.bind('KeyPress-Left'){
16
+ @remote.left
17
+ }
18
+ root.bind('KeyPress-Right'){
19
+ @remote.right
20
+ }
21
+ root.bind('KeyPress-Up'){
22
+ @remote.up
23
+ }
24
+ root.bind('KeyPress-Down'){
25
+ @remote.down
26
+ }
27
+ root.bind('KeyPress-Return'){
28
+ @remote.select
29
+ }
30
+ root.bind('KeyPress-space'){
31
+ @remote.pause
32
+ }
33
+ Tk.mainloop
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,117 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. ruku])
2
+ require 'rubygems'
3
+ require 'json'
4
+ require 'webrick'
5
+
6
+ include WEBrick
7
+
8
+ module Ruku
9
+ module Clients
10
+ class Web
11
+ attr_reader :options
12
+
13
+ def initialize(opts={})
14
+ @options = OpenStruct.new(opts)
15
+ handle_options
16
+
17
+ server_options = {
18
+ :Port => options.port || 3030,
19
+ :DocumentRoot => File.join(File.dirname(__FILE__), 'web_static')
20
+ }
21
+ @server = HTTPServer.new(server_options)
22
+
23
+ ['INT', 'TERM'].each do |signal|
24
+ trap(signal) { @server.shutdown }
25
+ end
26
+
27
+ @server.mount("/ajax", AjaxServlet)
28
+ end
29
+
30
+ def start
31
+ @server.start
32
+ end
33
+
34
+ # Parse and handle command line options
35
+ def handle_options
36
+ OptionParser.new do |opts|
37
+ opts.on('-p', '--port PORT') {|p| options.port = p.to_i }
38
+ opts.on('--web') { } # ignore
39
+ end.parse!
40
+ end
41
+
42
+ class AjaxServlet < HTTPServlet::AbstractServlet
43
+ def do_GET(req, resp)
44
+ @remote_manager ||= Remotes.new
45
+ @remote_manager.load
46
+
47
+ cmd = req.query['command']
48
+ action = req.query['action']
49
+ if cmd
50
+ resp.body = run_command(cmd, req.query['host'])
51
+ raise HTTPStatus::OK
52
+ elsif action
53
+ resp.body = perform_action(action, req.query['data'])
54
+ raise HTTPStatus::OK
55
+ else
56
+ raise HTTPStatus::PreconditionFailed.new("Missing parameter: 'command' or 'action'")
57
+ end
58
+ end
59
+
60
+ protected
61
+
62
+ # Send a regular remote command to the active remote or the remote with the specified host
63
+ def run_command(cmd, host=nil)
64
+ remote = host.nil? ? @remote_manager.active : @remote_manager.find_by_host(host)
65
+ if remote
66
+ remote.send_roku_command cmd
67
+ "success"
68
+ else
69
+ "error"
70
+ end
71
+ end
72
+
73
+ # Perform some remote management action
74
+ def perform_action(action, data)
75
+ if action == 'list'
76
+ # Get a list of known remotes
77
+ @remote_manager.remotes_to_json
78
+ elsif action == 'update'
79
+ @remote_manager.remotes_from_json(data)
80
+ @remote_manager.store
81
+ "success"
82
+ elsif action =~ /^scan/
83
+ # Scan the network for Roku boxes - two different action values are
84
+ # expected: scanForFirst or scanForAll
85
+
86
+ # With scanForFirst try to find only one box because that will be the common case
87
+ @remote_manager = Remotes.new(Remote.scan(action == 'scanForFirst'))
88
+ @remote_manager.active.name = 'My Roku Box' if @remote_manager.active
89
+ @remote_manager.store
90
+ @remote_manager.remotes_to_json
91
+ else
92
+ raise HTTPStatus::PreconditionFailed.new("Unknown action: '#{action}'")
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ class Remote
100
+ def to_json
101
+ "{\"host\":\"#{@host}\",\"name\":\"#{@name}\",\"port\":#{@port}}"
102
+ end
103
+ end
104
+
105
+ class Remotes
106
+ def remotes_to_json
107
+ "{\"remotes\":[#{ @boxes.map{|b| b.to_json}.join(',') }], \"active\":#{@active_index}}"
108
+ end
109
+
110
+ def remotes_from_json(json)
111
+ parsed = JSON.parse(json)
112
+ @boxes = []
113
+ parsed['remotes'].each {|r| @boxes << Remote.new(r['host'], r['name'], r['port'] || 8080) }
114
+ @active_index = parsed['active'] || 0
115
+ end
116
+ end
117
+ end