roku-ecp 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4596a3e8de44cf9e2828527b7c1db97b868226df2c142a939a673f6954bbeb5a
4
+ data.tar.gz: 445dd74fd2fea0890730b4daec31ce60ce0ec6eff4fa84a785b4a25ab0835a19
5
+ SHA512:
6
+ metadata.gz: e3870be57187c600a341aac7d53598c4981f31156e7b7a1230cbac3534f7daa9ffe8c388f0e0053df0295e74d35e27a42dd194a54aa95808af6b816e9a3a0b4b
7
+ data.tar.gz: f88c5ff06c91c9e0ec8cc093b86b874b453d042507999c0c2cd033e7cc50da59c2af1064c881a30e94c6827e4b9dcf975fd827bc4dbd6280b7742d47e9e06a95
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,15 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ httparty (0.16.2)
5
+ multi_xml (>= 0.5.2)
6
+ multi_xml (0.6.0)
7
+
8
+ PLATFORMS
9
+ ruby
10
+
11
+ DEPENDENCIES
12
+ httparty
13
+
14
+ BUNDLED WITH
15
+ 1.16.1
data/bin/console ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/roku'
4
+ require 'irb'
5
+ IRB.start
data/bin/roku ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/roku'
4
+
5
+ Roku::Input.new.run
data/lib/roku/app.rb ADDED
@@ -0,0 +1,13 @@
1
+ module Roku
2
+ App = Struct.new(:name, :id, :type, :version) do
3
+ def launch!
4
+ Client.launch(id)
5
+ end
6
+
7
+ class << self
8
+ def parse(hash)
9
+ App.new(*hash.values_at('__content__', 'id', 'type', 'version'))
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,101 @@
1
+ require 'httparty'
2
+
3
+ module Roku
4
+ module Client
5
+ include HTTParty
6
+ format :xml
7
+
8
+ KEYS = %i[
9
+ Home
10
+ Rev
11
+ Fwd
12
+ Play
13
+ Select
14
+ Left
15
+ Right
16
+ Down
17
+ Up
18
+ Back
19
+ InstantReplay
20
+ Info
21
+ Backspace
22
+ Search
23
+ Enter
24
+ FindRemote
25
+ VolumeDown
26
+ VolumeMute
27
+ VolumeUp
28
+ PowerOff
29
+ InputTuner
30
+ InputHDMI1
31
+ InputHDMI2
32
+ InputHDMI3
33
+ InputHDMI4
34
+ InputAV1
35
+ ].freeze
36
+
37
+ class << self
38
+ def find_device!
39
+ address = Roku::Discover.search
40
+ base_uri(address)
41
+ end
42
+
43
+ def apps
44
+ get('/query/apps').parsed_response['apps']['app'].map do |app|
45
+ App.parse(app)
46
+ end
47
+ end
48
+
49
+ def active_app
50
+ app = get('/query/active-app').parsed_response['active_app']['app']
51
+ App.parse(app)
52
+ end
53
+
54
+ def device_info
55
+ get('/query/device-info').parsed_response['device_info']
56
+ end
57
+
58
+ def tv_channels
59
+ get('/query/tv-channels').parsed_response['tv_channels']
60
+ end
61
+
62
+ def tv_active_channel
63
+ get('/query/tv-active-channel').parsed_response['tv_channel']
64
+ end
65
+
66
+ def launch(app_id)
67
+ post("/launch/#{app_id}").success?
68
+ end
69
+
70
+ def install(app_id)
71
+ post("/install/#{app_id}").success?
72
+ end
73
+
74
+ def send_text(string)
75
+ string.split('').map do |c|
76
+ next if c == ' '
77
+ post("/keypress/#{c}").success?
78
+ end.all?
79
+ end
80
+
81
+ def keypress(key)
82
+ return unless KEYS.include?(key.to_sym)
83
+ post("/keypress/#{key}").success?
84
+ end
85
+
86
+ def keydown(key)
87
+ return unless KEYS.include?(key.to_sym)
88
+ post("/keydown/#{key}").success?
89
+ end
90
+
91
+ def keyup(key)
92
+ return unless KEYS.include?(key.to_sym)
93
+ post("/keyup/#{key}").success?
94
+ end
95
+
96
+ def input(options = {})
97
+ post('/input', options)
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,56 @@
1
+ require 'socket'
2
+
3
+ module Roku
4
+ module Discover
5
+ MULTICAST_ADDR = '239.255.255.250'.freeze
6
+ BIND_ADDR = '0.0.0.0'.freeze
7
+ PORT = 1900
8
+ REQUEST = "M-SEARCH * HTTP/1.1\n" \
9
+ "Host: 239.255.255.250:1900\n" \
10
+ "Man: \"ssdp:discover\"\n" \
11
+ "ST: roku:ecp\n\n".freeze
12
+
13
+ class << self
14
+ def search
15
+ bind
16
+ socket.send(REQUEST, 0, MULTICAST_ADDR, PORT)
17
+ parse_address(await_response)
18
+ end
19
+
20
+ def await_response
21
+ Timeout.timeout(5) do
22
+ loop do
23
+ response, = socket.recvfrom(1024)
24
+ if response.include?('LOCATION') && response.include?('200 OK')
25
+ return response
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def bind
34
+ return if @bound
35
+ socket.bind(BIND_ADDR, PORT)
36
+ @bound = true
37
+ end
38
+
39
+ def parse_address(response)
40
+ response.scan(/LOCATION: (.*)\r/).first.first
41
+ end
42
+
43
+ def socket
44
+ @socket ||= UDPSocket.open.tap do |socket|
45
+ socket.setsockopt(:IPPROTO_IP, :IP_ADD_MEMBERSHIP, bind_address)
46
+ socket.setsockopt(:IPPROTO_IP, :IP_MULTICAST_TTL, 1)
47
+ socket.setsockopt(:SOL_SOCKET, :SO_REUSEPORT, 1)
48
+ end
49
+ end
50
+
51
+ def bind_address
52
+ IPAddr.new(MULTICAST_ADDR).hton + IPAddr.new(BIND_ADDR).hton
53
+ end
54
+ end
55
+ end
56
+ end
data/lib/roku/input.rb ADDED
@@ -0,0 +1,61 @@
1
+ require 'io/console'
2
+
3
+ module Roku
4
+ class Input
5
+ def run
6
+ while (input = read_char)
7
+ case input
8
+ when ' '
9
+ Roku::Client.keypress(:Play)
10
+ when "\r"
11
+ Roku::Client.keypress(:Select)
12
+ when "\e[A"
13
+ Roku::Client.keypress(:Up)
14
+ when "\e[B"
15
+ Roku::Client.keypress(:Down)
16
+ when "\e[C"
17
+ Roku::Client.keypress(:Right)
18
+ when "\e[D"
19
+ Roku::Client.keypress(:Left)
20
+ when "\u007F"
21
+ Roku::Client.keypress(:Back)
22
+ when "\e[1;5A"
23
+ Roku::Client.keypress(:VolumeUp)
24
+ when "\e[1;5B"
25
+ Roku::Client.keypress(:VolumeDown)
26
+ when 'a'
27
+ query = prompt('Launch: ')
28
+ app = Roku::Client.apps.find { |a| a.name.casecmp(query) }
29
+ if app.nil?
30
+ print "\rNo app found."
31
+ else
32
+ print "\rLaunching #{app.name}."
33
+ app.launch!
34
+ end
35
+ when 'q', "\u0003"
36
+ break
37
+ else
38
+ puts input.inspect
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def read_char
46
+ $stdin.raw do |stdin|
47
+ input = stdin.getc.chr
48
+ if input == "\e"
49
+ input << stdin.read_nonblock(3) rescue nil
50
+ input << stdin.read_nonblock(2) rescue nil
51
+ end
52
+ return input
53
+ end
54
+ end
55
+
56
+ def prompt(label)
57
+ print label
58
+ $stdin.gets.chomp
59
+ end
60
+ end
61
+ end
data/lib/roku.rb ADDED
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.push File.dirname(__FILE__)
2
+
3
+ require 'roku/app'
4
+ require 'roku/client'
5
+ require 'roku/input'
6
+ require 'roku/discover'
7
+
8
+ module Roku
9
+ end
data/readme.md ADDED
@@ -0,0 +1,43 @@
1
+ Roku External Control Protocol
2
+ ==============================
3
+
4
+ This library is a client for the Roku ECP, which allows you to control Roku
5
+ devices on your local network via http.
6
+
7
+ `Roku::Discover` allows you to find a Roku device on your network.
8
+
9
+ `Roku::Client` allows you to interact with Roku devices.
10
+
11
+ `Roku::Input` is a small terminal application that provides keyboard shortcuts
12
+ for interacting.
13
+
14
+
15
+ ## Usage
16
+
17
+ Find your Roku device and automatically configure the client to use it.
18
+
19
+ ```ruby
20
+ Roku::Client.find_device!
21
+ # => "http://192.168.0.106:8060/"
22
+ ```
23
+
24
+ Begin interacting
25
+
26
+ ```ruby
27
+ Roku::Client.active_app
28
+ # => #<Struct:Roku::App:0x5611ec31c2a8
29
+ # id = "13535"
30
+ # name = "Plex"
31
+ # type = "appl"
32
+ # version = "5.3.4"
33
+
34
+ Roku::Client.keypress(:Play)
35
+ # => true
36
+ ```
37
+
38
+ View lib/roku/client.rb for complete list of buttons presses.
39
+
40
+ ## Limitations / TODO
41
+
42
+ This approach only supports one device at a time. I only have one device, so I
43
+ can't test a scenario where multiple devices can be found on the network.
data/roku-ecp.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = 'roku-ecp'
6
+ spec.version = '0.1.0'
7
+ spec.authors = ['Jacob Evan Shreve']
8
+ spec.email = ['github@shreve.io']
9
+
10
+ spec.summary = 'A library for controlling Roku devices on your local network'
11
+ spec.homepage = 'https://github.com/shreve/roku-ecp'
12
+ spec.license = 'MIT'
13
+
14
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
15
+ f.match(%r{^(test|spec|features)/})
16
+ end
17
+
18
+ spec.bindir = 'bin'
19
+ spec.executables = ['roku']
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_runtime_dependency 'httparty', '~> 0.16.2'
23
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: roku-ecp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jacob Evan Shreve
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-07-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: httparty
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.16.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.16.2
27
+ description:
28
+ email:
29
+ - github@shreve.io
30
+ executables:
31
+ - roku
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - Gemfile
36
+ - Gemfile.lock
37
+ - bin/console
38
+ - bin/roku
39
+ - lib/roku.rb
40
+ - lib/roku/app.rb
41
+ - lib/roku/client.rb
42
+ - lib/roku/discover.rb
43
+ - lib/roku/input.rb
44
+ - readme.md
45
+ - roku-ecp.gemspec
46
+ homepage: https://github.com/shreve/roku-ecp
47
+ licenses:
48
+ - MIT
49
+ metadata: {}
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubyforge_project:
66
+ rubygems_version: 2.7.6
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: A library for controlling Roku devices on your local network
70
+ test_files: []