hive-runner-tv 0.1.2
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 +7 -0
- data/README.md +53 -0
- data/lib/hive/controller/tv.rb +48 -0
- data/lib/hive/device/tv.rb +13 -0
- data/lib/hive/diagnostic/tv/dead.rb +44 -0
- data/lib/hive/diagnostic/tv/uptime.rb +31 -0
- data/lib/hive/messages/tv_job.rb +20 -0
- data/lib/hive/worker/tv.rb +226 -0
- metadata +92 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b6afb887754f6f8863ac063c54724322aeb1ea80
|
4
|
+
data.tar.gz: a3d420f7922168d075b88c4a6b6e34e2055fb735
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2a2fbaaf867d636638972f7c99c21921a8ec97b1125fea6455bdd9af9ccf8973ca733ebe4c4e936bb9a006367c31509f31b93c22d67eab5c8d117adca6480862
|
7
|
+
data.tar.gz: 3a9526b391be926e1a8d3aa5d4bf7053732ee498c68bdfb1126363ae8eed2a790a7a8a8b1f4374c815a60bb877b45aabec9e78607a4600625bb92b5953432cd5
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# hive-runner-tv
|
2
|
+
TV module for Hive Runner
|
3
|
+
|
4
|
+
## Configuration file
|
5
|
+
|
6
|
+
```
|
7
|
+
controllers:
|
8
|
+
tv:
|
9
|
+
ir_blaster_clients:
|
10
|
+
<devicedb id>:
|
11
|
+
type: <Blaster type, eg rat_blaster>
|
12
|
+
mac: <IR blaster mac address, eg 00-11-22-33-44-55>
|
13
|
+
dataset: <IR blaster dataset name>
|
14
|
+
output: <IP blaster output id>
|
15
|
+
sequences:
|
16
|
+
launch_titantv:
|
17
|
+
- signal:Power
|
18
|
+
- sleep:2
|
19
|
+
- signal:Power
|
20
|
+
|
21
|
+
network:
|
22
|
+
remote_talkshow_address: <talkshow url, eg talkshow.remote>
|
23
|
+
remote_talkshow_port_offset: <minimum port number>
|
24
|
+
tv:
|
25
|
+
titantv_url: <eg http://titantv.url/titantv>
|
26
|
+
titantv_name: <Application name reported by Titan TV>
|
27
|
+
ir_blaster_host: <ip address of IR blaster hub, eg, 10.20.30.40>
|
28
|
+
ir_blaster_port: <port number of IR blaster hub>
|
29
|
+
|
30
|
+
diagnostics:
|
31
|
+
tv:
|
32
|
+
uptime:
|
33
|
+
reboot_time: <seconds between reboots>
|
34
|
+
dead:
|
35
|
+
```
|
36
|
+
|
37
|
+
Note, the `ir\_blaster\_clients` section under `controllers` may ultimately
|
38
|
+
move to the information provided by the Device Database.
|
39
|
+
|
40
|
+
## IR Blaster
|
41
|
+
|
42
|
+
To use an IR blaster with `hive-runner` add the line
|
43
|
+
|
44
|
+
gem 'device_api-tv', git: 'git@github.com:bbc/device_api-tv'
|
45
|
+
|
46
|
+
to the Hive's Gemfile and add the configuration as shown above.
|
47
|
+
|
48
|
+
## License
|
49
|
+
|
50
|
+
Hive Runner TV is available to everyone under the terms of the MIT open source licence.
|
51
|
+
Take a look at the LICENSE file in the code.
|
52
|
+
|
53
|
+
Copyright (c) 2016 BBC
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'hive/controller'
|
2
|
+
require 'hive/worker/tv'
|
3
|
+
|
4
|
+
module Hive
|
5
|
+
class Controller
|
6
|
+
# The TV controller
|
7
|
+
class Tv < Controller
|
8
|
+
@@exclusion = []
|
9
|
+
|
10
|
+
def self.add_exclusion ex
|
11
|
+
@@exclusion << ex
|
12
|
+
end
|
13
|
+
|
14
|
+
def detect
|
15
|
+
Hive.logger.debug("Checking Hive Mind")
|
16
|
+
device_list = Hive.hive_mind.device_details['connected_devices']
|
17
|
+
Hive.logger.debug("Device list: #{device_list}")
|
18
|
+
devices = []
|
19
|
+
if device_list.is_a? Array
|
20
|
+
device_list.select { |d| valid? d }.collect do |device|
|
21
|
+
Hive.logger.debug("Found TV: #{device.inspect}")
|
22
|
+
devices << self.create_device(device)
|
23
|
+
end
|
24
|
+
else
|
25
|
+
raise Hive::Controller::DeviceDetectionFailed
|
26
|
+
end
|
27
|
+
Hive.logger.debug("Devices: #{devices}")
|
28
|
+
devices
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def valid? device
|
33
|
+
if device['device_type'] == 'Tv'
|
34
|
+
@@exclusion.each do |ex|
|
35
|
+
match = true
|
36
|
+
ex.each_pair do |key, value|
|
37
|
+
match = match && device[key.to_s] != value
|
38
|
+
end
|
39
|
+
return false if ! match
|
40
|
+
end
|
41
|
+
return true
|
42
|
+
else
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'hive/diagnostic'
|
2
|
+
|
3
|
+
module Hive
|
4
|
+
class Diagnostic
|
5
|
+
class Tv
|
6
|
+
class Dead < Diagnostic
|
7
|
+
def diagnose
|
8
|
+
status = @device_api.status
|
9
|
+
case status
|
10
|
+
when 'idle'
|
11
|
+
self.pass('Device is idle')
|
12
|
+
else
|
13
|
+
self.fail("Device is #{status}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def repair(result)
|
18
|
+
tries = [
|
19
|
+
[ :launch_titantv ],
|
20
|
+
[ :power_cycle, :launch_titantv ],
|
21
|
+
[ :power_on, :launch_titantv ]
|
22
|
+
]
|
23
|
+
|
24
|
+
tries.each do |commands|
|
25
|
+
catch :commands_failed do
|
26
|
+
commands.each do |command|
|
27
|
+
throw :commands_failed if ! @device_api.run_sequence(command)
|
28
|
+
end
|
29
|
+
timeout = Time.now + 600
|
30
|
+
while Time.now < timeout
|
31
|
+
if @device_api.status == 'idle'
|
32
|
+
return self.pass('Titan TV launched')
|
33
|
+
end
|
34
|
+
sleep 5
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
self.fail("Failed to recover device")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'hive/diagnostic'
|
2
|
+
|
3
|
+
module Hive
|
4
|
+
class Diagnostic
|
5
|
+
class Tv
|
6
|
+
class Uptime < Diagnostic
|
7
|
+
def initialize(config, options)
|
8
|
+
@next_reboot_time = Time.now + config[:reboot_timeout] if config.has_key?(:reboot_timeout)
|
9
|
+
super(config, options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def diagnose
|
13
|
+
if config.has_key?(:reboot_timeout)
|
14
|
+
if Time.now < @next_reboot_time
|
15
|
+
self.pass("Time to next reboot: #{@next_reboot_time - Time.now}s")
|
16
|
+
else
|
17
|
+
self.fail("Reboot required")
|
18
|
+
end
|
19
|
+
else
|
20
|
+
self.pass("Not configured to reboot")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def repair(result)
|
25
|
+
@next_reboot_time += config[:reboot_timeout]
|
26
|
+
@device_api.power_cycle ? self.pass("Successful reboot") : self.fail("Reboot failed")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'hive/messages'
|
4
|
+
|
5
|
+
module Hive
|
6
|
+
module Messages
|
7
|
+
class TvJob < Hive::Messages::Job
|
8
|
+
# TODO Fix this validation
|
9
|
+
# validates :application_url, :application_url_parameters, presence: true
|
10
|
+
|
11
|
+
def application_url
|
12
|
+
self.target.symbolize_keys[:application_url]
|
13
|
+
end
|
14
|
+
|
15
|
+
def application_url_parameters
|
16
|
+
self.target.symbolize_keys[:application_url_parameters]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'hive/worker'
|
2
|
+
require 'hive/messages/tv_job'
|
3
|
+
require 'mind_meld/tv'
|
4
|
+
require 'talkshow'
|
5
|
+
|
6
|
+
module Hive
|
7
|
+
class Worker
|
8
|
+
# The TV worker
|
9
|
+
class Tv < Worker
|
10
|
+
class FailedRedirect < StandardError
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(config)
|
14
|
+
@brand = config['brand'].downcase.gsub(/\s/, '_')
|
15
|
+
@model = config['model'].downcase.gsub(/\s/, '_')
|
16
|
+
|
17
|
+
if config['ir_blaster_clients'] and config['ir_blaster_clients'].has_key?(config['id'])
|
18
|
+
require 'device_api/tv'
|
19
|
+
DeviceAPI::RatBlaster.configure do |rb_config|
|
20
|
+
rb_config.host = Hive.config.network.tv.ir_blaster_host if Hive.config.network.tv.ir_blaster_host?
|
21
|
+
rb_config.port = Hive.config.network.tv.ir_blaster_port if Hive.config.network.tv.ir_blaster_port?
|
22
|
+
end
|
23
|
+
|
24
|
+
config.merge!({"device_api" => DeviceAPI::TV::Device.new(
|
25
|
+
id: config['id'],
|
26
|
+
ir: {
|
27
|
+
type: config['ir_blaster_clients'][config['id']].type,
|
28
|
+
mac: config['ir_blaster_clients'][config['id']].mac,
|
29
|
+
dataset: config['ir_blaster_clients'][config['id']].dataset,
|
30
|
+
output: config['ir_blaster_clients'][config['id']].output
|
31
|
+
}
|
32
|
+
)})
|
33
|
+
config['ir_blaster_clients'][config['id']].sequences.each do |name, pattern|
|
34
|
+
config['device_api'].set_sequence(name.to_sym, pattern)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
super(config)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Prepare the TV
|
41
|
+
def pre_script(job, job_paths, script)
|
42
|
+
url = job.application_url
|
43
|
+
@log.info("Application url: #{url}")
|
44
|
+
# TODO Set device as busy in Hive mind
|
45
|
+
|
46
|
+
params = ""
|
47
|
+
|
48
|
+
# 'cross_network_restriction' means that the device requires Talkshow
|
49
|
+
# on the same network as the application.
|
50
|
+
# For the moment, assume networks are '10.10.*.*' and'*.bbc.co.uk'
|
51
|
+
# TODO Make this more general
|
52
|
+
if @options['features'] && @options['features'].include?('cross_network_restriction') and /bbc.co.uk/.match url
|
53
|
+
ts_address = Hive.config.network.remote_talkshow_address
|
54
|
+
ts_port = @ts_port = Hive.config.network.remote_talkshow_port_offset + @options['id']
|
55
|
+
@log.info("Using remote talkshow on port #{ts_port}")
|
56
|
+
script.set_env 'TALKSHOW_REMOTE_URL', "http://#{ts_address}:#{ts_port}"
|
57
|
+
# Not actually required but talkshow fails without it set
|
58
|
+
script.set_env 'TALKSHOW_PORT', ts_port
|
59
|
+
else
|
60
|
+
ts_port = @ts_port = self.allocate_port
|
61
|
+
@log.info("Using talkshow on port #{ts_port}")
|
62
|
+
# TODO Move this more centrally
|
63
|
+
ip = Socket.ip_address_list.detect { |intf| intf.ipv4_private? }
|
64
|
+
ts_address = ip.ip_address
|
65
|
+
script.set_env 'TALKSHOW_PORT', ts_port
|
66
|
+
end
|
67
|
+
@log.info("Talkshow server address is: #{ts_address}")
|
68
|
+
|
69
|
+
if job.application_url_parameters.present?
|
70
|
+
params = "?#{job.application_url_parameters}&talkshowurl=#{ts_address}:#{ts_port}"
|
71
|
+
else
|
72
|
+
params = "?talkshowurl=#{ts_address}:#{ts_port}"
|
73
|
+
end
|
74
|
+
url += params
|
75
|
+
|
76
|
+
self.redirect(
|
77
|
+
url: url,
|
78
|
+
old_app: Hive.config.network.tv.titantv_name,
|
79
|
+
skip_first_load: true
|
80
|
+
)
|
81
|
+
load_hive_mind ts_port, url
|
82
|
+
|
83
|
+
@log.info("Starting TV Application monitor")
|
84
|
+
@monitor = Thread.new do
|
85
|
+
loop do
|
86
|
+
poll_response = Hive.hive_mind.poll(@device_id)
|
87
|
+
# if poll_response.is_a? Array and poll_response.length > 0
|
88
|
+
# @log.debug("[TV app monitor] Polled TV. Application = #{poll_response.first['application']}")
|
89
|
+
# # if @hive_mind.device_details(refresh: true)['application'] == Hive.config.network.tv.titantv_name
|
90
|
+
# if poll_response.first['application'] == Hive.config.network.tv.titantv_name
|
91
|
+
# # TV has returned to the holding app
|
92
|
+
# # Put back in the app under test
|
93
|
+
# self.redirect(
|
94
|
+
# url: url,
|
95
|
+
# old_app: Hive.config.network.tv.titantv_name,
|
96
|
+
# log_prefix: '[TV app monitor] '
|
97
|
+
# )
|
98
|
+
# end
|
99
|
+
# else
|
100
|
+
# @log.warn("[TV app monitor] Failed to poll TV")
|
101
|
+
# end
|
102
|
+
sleep 20
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
return nil
|
107
|
+
end
|
108
|
+
|
109
|
+
def job_message_klass
|
110
|
+
Hive::Messages::TvJob
|
111
|
+
end
|
112
|
+
|
113
|
+
def mind_meld_klass
|
114
|
+
MindMeld::Tv
|
115
|
+
end
|
116
|
+
|
117
|
+
def post_script(job, job_paths, script)
|
118
|
+
self.release_port(@ts_port) if @ts_port
|
119
|
+
|
120
|
+
signal_safe_post_script(job, job_paths, script)
|
121
|
+
end
|
122
|
+
|
123
|
+
def signal_safe_post_script(job, job_paths, script)
|
124
|
+
@log.info('Terminating TV Application monitor')
|
125
|
+
@monitor.exit if @monitor
|
126
|
+
|
127
|
+
self.redirect(url: Hive.config.network.tv.titantv_url, new_app: Hive.config.network.tv.titantv_name, skip_last_load: true)
|
128
|
+
# TODO Set device as idle in Hive Mind
|
129
|
+
end
|
130
|
+
|
131
|
+
def device_status
|
132
|
+
@hive_mind.device_details['status']
|
133
|
+
end
|
134
|
+
|
135
|
+
def set_device_status(status)
|
136
|
+
# TODO Set status from Hive Mind
|
137
|
+
# At the moment the device state cannot be changed
|
138
|
+
device_status
|
139
|
+
end
|
140
|
+
|
141
|
+
def autogenerated_queues
|
142
|
+
[ "#{@brand}-#{@model}" ]
|
143
|
+
end
|
144
|
+
|
145
|
+
def redirect(opts)
|
146
|
+
raise ArgumentError if ! ( opts.has_key?(:url) && ( opts.has_key?(:old_app) || opts.has_key?(:new_app) ) )
|
147
|
+
load_hive_mind(@ts_port, opts[:old_app] || "Trying to redirect ...") if ! opts[:skip_first_load]
|
148
|
+
opts[:log_prefix] ||= ''
|
149
|
+
@log.info("#{opts[:log_prefix]}Redirecting to #{opts[:url]}")
|
150
|
+
@hive_mind.create_action(action_type: 'redirect', body: opts[:url])
|
151
|
+
sleep 5
|
152
|
+
load_hive_mind(@ts_port, opts[:url]) if ! opts[:skip_last_load]
|
153
|
+
|
154
|
+
max_wait_count = 30
|
155
|
+
wait_count = 0
|
156
|
+
max_retry_count = 15
|
157
|
+
retry_count = 0
|
158
|
+
|
159
|
+
app_name = @hive_mind.device_details(refresh: true)['application']
|
160
|
+
@log.debug("#{opts[:log_prefix]}Current app: #{app_name}")
|
161
|
+
while (opts.has_key?(:new_app) && app_name != opts[:new_app]) || (opts.has_key?(:old_app) && app_name == opts[:old_app])
|
162
|
+
if wait_count >= max_wait_count
|
163
|
+
if retry_count >= max_retry_count
|
164
|
+
raise FailedRedirect
|
165
|
+
else
|
166
|
+
retry_count += 1
|
167
|
+
wait_count = 0
|
168
|
+
@log.info("#{opts[:log_prefix]}Redirecting to #{opts[:url]} [#{retry_count}]")
|
169
|
+
@hive_mind.create_action(action_type: 'redirect', body: opts[:url])
|
170
|
+
sleep 5
|
171
|
+
end
|
172
|
+
else
|
173
|
+
wait_count += 1
|
174
|
+
@log.info("#{opts[:log_prefix]} . [#{wait_count}]")
|
175
|
+
sleep 5
|
176
|
+
load_hive_mind(@ts_port, opts[:url]) if ! opts[:skip_last_load]
|
177
|
+
end
|
178
|
+
app_name = @hive_mind.device_details(refresh: true)['application']
|
179
|
+
@log.debug("#{opts[:log_prefix]}Current app: #{app_name}")
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Between tests the TV must be in the holding app
|
184
|
+
def diagnostics
|
185
|
+
app_name = @hive_mind.device_details(refresh: true)['application']
|
186
|
+
raise DeviceNotReady.new("Current application: '#{app_name}'") if app_name != Hive.config.network.tv.titantv_name
|
187
|
+
super
|
188
|
+
end
|
189
|
+
|
190
|
+
def load_hive_mind ts_port, app_name
|
191
|
+
ts = Talkshow.new
|
192
|
+
@log.info("Port: #{ts_port}")
|
193
|
+
@log.info("App: #{app_name}")
|
194
|
+
@log.info("Logfile: #{@file_system.results_path}/talkshowserver.log")
|
195
|
+
@log.info("titantv_url: #{Hive.config.network.tv.titantv_url}")
|
196
|
+
ts.start_server(port: ts_port, logfile: "#{@file_system.results_path}/talkshowserver.log")
|
197
|
+
5.times do
|
198
|
+
begin
|
199
|
+
ts.execute <<JS
|
200
|
+
(function(){
|
201
|
+
var load_script = document.createElement('script');
|
202
|
+
load_script.type = 'text/javascript';
|
203
|
+
load_script.charset = 'utf-8';
|
204
|
+
load_script.src = '#{Hive.config.network.tv.titantv_url}/script/hive_mind_com.js';
|
205
|
+
document.getElementsByTagName('head')[0].appendChild(load_script);
|
206
|
+
// Give it 10 seconds to load
|
207
|
+
// TODO Do this with a retry
|
208
|
+
setTimeout(function() {
|
209
|
+
hive_mind_com.init('#{app_name}', '#{Hive.config.network.tv.titantv_url}');
|
210
|
+
hive_mind_com.start();
|
211
|
+
}, 10000);
|
212
|
+
return true;
|
213
|
+
})()
|
214
|
+
JS
|
215
|
+
break
|
216
|
+
rescue Talkshow::Timeout
|
217
|
+
@log.info("Talkshow timeout")
|
218
|
+
sleep 5
|
219
|
+
end
|
220
|
+
end
|
221
|
+
ts.stop_server
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hive-runner-tv
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joe Haig
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-09-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: hive-runner
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: talkshow
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.4.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.4.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: res
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.2'
|
55
|
+
description: The TV controller module for Hive Runner
|
56
|
+
email: joe.haig@bbc.co.uk
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- README.md
|
62
|
+
- lib/hive/controller/tv.rb
|
63
|
+
- lib/hive/device/tv.rb
|
64
|
+
- lib/hive/diagnostic/tv/dead.rb
|
65
|
+
- lib/hive/diagnostic/tv/uptime.rb
|
66
|
+
- lib/hive/messages/tv_job.rb
|
67
|
+
- lib/hive/worker/tv.rb
|
68
|
+
homepage: https://github.com/bbc-test/hive-runner-tv
|
69
|
+
licenses:
|
70
|
+
- MIT
|
71
|
+
metadata: {}
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 2.5.0
|
89
|
+
signing_key:
|
90
|
+
specification_version: 4
|
91
|
+
summary: Hive Runner TV
|
92
|
+
test_files: []
|