ruku 0.1

Sign up to get free protection for your applications and to get access to all the features.
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