roku-ecp 0.1.0

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.
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: []