playful 0.1.0.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +19 -0
- data/.rspec +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/History.rdoc +3 -0
- data/LICENSE.rdoc +22 -0
- data/README.rdoc +194 -0
- data/Rakefile +20 -0
- data/features/control_point.feature +13 -0
- data/features/device.feature +22 -0
- data/features/device_discovery.feature +9 -0
- data/features/step_definitions/control_point_steps.rb +19 -0
- data/features/step_definitions/device_discovery_steps.rb +40 -0
- data/features/step_definitions/device_steps.rb +28 -0
- data/features/support/common.rb +9 -0
- data/features/support/env.rb +17 -0
- data/features/support/fake_upnp_device_collection.rb +108 -0
- data/features/support/world_extensions.rb +15 -0
- data/lib/core_ext/hash_patch.rb +5 -0
- data/lib/core_ext/socket_patch.rb +16 -0
- data/lib/core_ext/to_upnp_s.rb +65 -0
- data/lib/playful.rb +5 -0
- data/lib/playful/control_point.rb +175 -0
- data/lib/playful/control_point/base.rb +74 -0
- data/lib/playful/control_point/device.rb +511 -0
- data/lib/playful/control_point/error.rb +13 -0
- data/lib/playful/control_point/service.rb +404 -0
- data/lib/playful/device.rb +28 -0
- data/lib/playful/logger.rb +8 -0
- data/lib/playful/ssdp.rb +195 -0
- data/lib/playful/ssdp/broadcast_searcher.rb +114 -0
- data/lib/playful/ssdp/error.rb +6 -0
- data/lib/playful/ssdp/listener.rb +38 -0
- data/lib/playful/ssdp/multicast_connection.rb +112 -0
- data/lib/playful/ssdp/network_constants.rb +17 -0
- data/lib/playful/ssdp/notifier.rb +41 -0
- data/lib/playful/ssdp/searcher.rb +87 -0
- data/lib/playful/version.rb +3 -0
- data/lib/rack/upnp_control_point.rb +70 -0
- data/playful.gemspec +38 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/search_responses.rb +134 -0
- data/spec/unit/core_ext/to_upnp_s_spec.rb +105 -0
- data/spec/unit/playful/control_point/device_spec.rb +7 -0
- data/spec/unit/playful/control_point_spec.rb +45 -0
- data/spec/unit/playful/ssdp/listener_spec.rb +29 -0
- data/spec/unit/playful/ssdp/multicast_connection_spec.rb +157 -0
- data/spec/unit/playful/ssdp/notifier_spec.rb +76 -0
- data/spec/unit/playful/ssdp/searcher_spec.rb +110 -0
- data/spec/unit/playful/ssdp_spec.rb +214 -0
- data/tasks/control_point.html +30 -0
- data/tasks/control_point.thor +43 -0
- data/tasks/search.thor +128 -0
- data/tasks/test_js/FABridge.js +1425 -0
- data/tasks/test_js/WebSocketMain.swf +807 -0
- data/tasks/test_js/swfobject.js +825 -0
- data/tasks/test_js/web_socket.js +1133 -0
- data/test/test_ssdp.rb +298 -0
- data/test/test_ssdp_notification.rb +74 -0
- data/test/test_ssdp_response.rb +31 -0
- data/test/test_ssdp_search.rb +23 -0
- metadata +339 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'log_buddy'
|
3
|
+
require_relative '../../lib/playful/control_point'
|
4
|
+
|
5
|
+
def local_ip_and_port
|
6
|
+
orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true
|
7
|
+
|
8
|
+
UDPSocket.open do |s|
|
9
|
+
s.connect '64.233.187.99', 1
|
10
|
+
s.addr.last
|
11
|
+
[s.addr.last, s.addr[1]]
|
12
|
+
end
|
13
|
+
ensure
|
14
|
+
Socket.do_not_reverse_lookup = orig
|
15
|
+
end
|
16
|
+
|
17
|
+
ENV['RUBY_UPNP_ENV'] = 'testing'
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'socket'
|
3
|
+
require 'ipaddr'
|
4
|
+
|
5
|
+
require_relative '../../lib/playful/ssdp/network_constants'
|
6
|
+
|
7
|
+
class FakeUPnPDeviceCollection
|
8
|
+
include Singleton
|
9
|
+
include UPnP::SSDP::NetworkConstants
|
10
|
+
|
11
|
+
attr_accessor :respond_with
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@response = ''
|
15
|
+
@ssdp_listen_thread = nil
|
16
|
+
@serve_description = false
|
17
|
+
@local_ip, @local_port = local_ip_and_port
|
18
|
+
end
|
19
|
+
|
20
|
+
def expect_discovery(type)
|
21
|
+
case type
|
22
|
+
when :m_search
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
def stop_ssdp_listening
|
29
|
+
puts "<#{self.class}> Stopping..."
|
30
|
+
@ssdp_listen_thread.kill if @ssdp_listen_thread && @ssdp_listen_thread.alive?
|
31
|
+
puts "<#{self.class}> Stopped."
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Thread] The thread that's doing the listening.
|
35
|
+
def start_ssdp_listening
|
36
|
+
multicast_socket = setup_multicast_socket
|
37
|
+
multicast_socket.bind(MULTICAST_IP, MULTICAST_PORT)
|
38
|
+
|
39
|
+
ttl = [4].pack 'i'
|
40
|
+
unicast_socket = UDPSocket.open
|
41
|
+
unicast_socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, ttl)
|
42
|
+
|
43
|
+
@ssdp_listen_thread = Thread.new do
|
44
|
+
loop do
|
45
|
+
text, sender = multicast_socket.recvfrom(1024)
|
46
|
+
#puts "<#{self.class}> received text:\n#{text} from #{sender}"
|
47
|
+
|
48
|
+
#if text =~ /ST: upnp:rootdevice/
|
49
|
+
#if text =~ /#{@response}/m
|
50
|
+
if text =~ /M-SEARCH.*#{@local_ip}/m
|
51
|
+
puts "<#{self.class}> received text:\n#{text} from #{sender}"
|
52
|
+
return_port, return_ip = sender[1], sender[2]
|
53
|
+
|
54
|
+
puts "<#{self.class}> sending response\n#{@response}\n back to: #{return_ip}:#{return_port}"
|
55
|
+
unicast_socket.send(@response, 0, return_ip, return_port)
|
56
|
+
#multicast_socket.close
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def start_serving_description
|
63
|
+
=begin
|
64
|
+
tcp_server = TCPServer.new('0.0.0.0', 4567)
|
65
|
+
@serve_description = true
|
66
|
+
|
67
|
+
while @serve_description
|
68
|
+
@description_serve_thread = Thread.start(tcp_server.accept) do |s|
|
69
|
+
print(s, " is accepted\n")
|
70
|
+
s.write(Time.now)
|
71
|
+
print(s, " is gone\n")
|
72
|
+
s.close
|
73
|
+
end
|
74
|
+
end
|
75
|
+
=end
|
76
|
+
require 'webrick'
|
77
|
+
@description_server = WEBrick::HTTPServer.new(Port: 4567)
|
78
|
+
trap('INT') { @description_server.shutdown }
|
79
|
+
@description_server.mount_proc '/' do |req, res|
|
80
|
+
res.body = "<start>\n</start>"
|
81
|
+
end
|
82
|
+
@description_server.start
|
83
|
+
end
|
84
|
+
|
85
|
+
def stop_serving_description
|
86
|
+
=begin
|
87
|
+
@serve_description = false
|
88
|
+
|
89
|
+
if @description_serve_thread && @description_serve_thread.alive?
|
90
|
+
@description_serve_thread.join
|
91
|
+
end
|
92
|
+
=end
|
93
|
+
@description_server.stop
|
94
|
+
end
|
95
|
+
|
96
|
+
def setup_multicast_socket
|
97
|
+
membership = IPAddr.new(MULTICAST_IP).hton + IPAddr.new('0.0.0.0').hton
|
98
|
+
ttl = [4].pack 'i'
|
99
|
+
|
100
|
+
socket = UDPSocket.new
|
101
|
+
socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership)
|
102
|
+
socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, "\000")
|
103
|
+
socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, ttl)
|
104
|
+
socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_TTL, ttl)
|
105
|
+
|
106
|
+
socket
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module HelperStuff
|
2
|
+
def control_point
|
3
|
+
@control_point ||= UPnP::ControlPoint.new
|
4
|
+
end
|
5
|
+
|
6
|
+
def fake_device_collection
|
7
|
+
@fake_device_collection ||= FakeUPnPDeviceCollection.instance
|
8
|
+
end
|
9
|
+
|
10
|
+
def local_ip
|
11
|
+
@local_ip ||= local_ip_and_port.first
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
World(HelperStuff)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
# Workaround for missing constants on Windows
|
4
|
+
module Socket::Constants
|
5
|
+
IP_ADD_MEMBERSHIP = 12 unless defined? IP_ADD_MEMBERSHIP
|
6
|
+
IP_MULTICAST_LOOP = 11 unless defined? IP_MULTICAST_LOOP
|
7
|
+
IP_MULTICAST_TTL = 10 unless defined? IP_MULTICAST_TTL
|
8
|
+
IP_TTL = 4 unless defined? IP_TTL
|
9
|
+
end
|
10
|
+
|
11
|
+
class Socket
|
12
|
+
IP_ADD_MEMBERSHIP = 12 unless defined? IP_ADD_MEMBERSHIP
|
13
|
+
IP_MULTICAST_LOOP = 11 unless defined? IP_MULTICAST_LOOP
|
14
|
+
IP_MULTICAST_TTL = 10 unless defined? IP_MULTICAST_TTL
|
15
|
+
IP_TTL = 4 unless defined? IP_TTL
|
16
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class Hash
|
2
|
+
|
3
|
+
# Converts Hash search targets to SSDP search target String. Conversions are
|
4
|
+
# as follows:
|
5
|
+
# uuid: "someUUID" # => "uuid:someUUID"
|
6
|
+
# device_type: "someDeviceType:1" # => "urn:schemas-upnp-org:device:someDeviceType:1"
|
7
|
+
# service_type: "someServiceType:2" # => "urn:schemas-upnp-org:service:someServiceType:2"
|
8
|
+
#
|
9
|
+
# You can use custom UPnP domain names too:
|
10
|
+
# { device_type: "someDeviceType:3",
|
11
|
+
# domain_name: "mydomain-com" } # => "urn:my-domain:device:someDeviceType:3"
|
12
|
+
# { service_type: "someServiceType:4",
|
13
|
+
# domain_name: "mydomain-com" } # => "urn:my-domain:service:someDeviceType:4"
|
14
|
+
#
|
15
|
+
# @return [String] The converted String, according to the UPnP spec.
|
16
|
+
def to_upnp_s
|
17
|
+
if self.has_key? :uuid
|
18
|
+
return "uuid:#{self[:uuid]}"
|
19
|
+
elsif self.has_key? :device_type
|
20
|
+
if self.has_key? :domain_name
|
21
|
+
return "urn:#{self[:domain_name]}:device:#{self[:device_type]}"
|
22
|
+
else
|
23
|
+
return "urn:schemas-upnp-org:device:#{self[:device_type]}"
|
24
|
+
end
|
25
|
+
elsif self.has_key? :service_type
|
26
|
+
if self.has_key? :domain_name
|
27
|
+
return "urn:#{self[:domain_name]}:service:#{self[:service_type]}"
|
28
|
+
else
|
29
|
+
return "urn:schemas-upnp-org:service:#{self[:service_type]}"
|
30
|
+
end
|
31
|
+
else
|
32
|
+
self.to_s
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
class Symbol
|
39
|
+
|
40
|
+
# Converts Symbol search targets to SSDP search target String. Conversions are
|
41
|
+
# as follows:
|
42
|
+
# :all # => "ssdp:all"
|
43
|
+
# :root # => "upnp:rootdevice"
|
44
|
+
# "root" # => "upnp:rootdevice"
|
45
|
+
#
|
46
|
+
# @return [String] The converted String, according to the UPnP spec.
|
47
|
+
def to_upnp_s
|
48
|
+
if self == :all
|
49
|
+
'ssdp:all'
|
50
|
+
elsif self == :root
|
51
|
+
'upnp:rootdevice'
|
52
|
+
else
|
53
|
+
self
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
class String
|
60
|
+
# This doesn't do anything to the string; just allows users to call the
|
61
|
+
# method without having to check type first.
|
62
|
+
def to_upnp_s
|
63
|
+
self
|
64
|
+
end
|
65
|
+
end
|
data/lib/playful.rb
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'nori'
|
3
|
+
require 'em-synchrony'
|
4
|
+
require_relative 'logger'
|
5
|
+
require_relative 'ssdp'
|
6
|
+
require_relative 'control_point/service'
|
7
|
+
require_relative 'control_point/device'
|
8
|
+
require_relative 'control_point/error'
|
9
|
+
|
10
|
+
|
11
|
+
module Playful
|
12
|
+
|
13
|
+
# Allows for controlling a UPnP device as defined in the UPnP spec for control
|
14
|
+
# points.
|
15
|
+
#
|
16
|
+
# It uses +Nori+ for parsing the description XML files, which will use +Nokogiri+
|
17
|
+
# if you have it installed.
|
18
|
+
class ControlPoint
|
19
|
+
include LogSwitch::Mixin
|
20
|
+
|
21
|
+
def self.config
|
22
|
+
yield self
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
attr_accessor :raise_on_remote_error
|
27
|
+
end
|
28
|
+
|
29
|
+
@@raise_on_remote_error ||= true
|
30
|
+
|
31
|
+
attr_reader :devices
|
32
|
+
|
33
|
+
# @param [String] search_target The device(s) to control.
|
34
|
+
# @param [Hash] search_options Options to pass on to SSDP search and listen calls.
|
35
|
+
# @option options [Fixnum] response_wait_time
|
36
|
+
# @option options [Fixnum] m_search_count
|
37
|
+
# @option options [Fixnum] ttl
|
38
|
+
def initialize(search_target, search_options = {})
|
39
|
+
@search_target = search_target
|
40
|
+
@search_options = search_options
|
41
|
+
@search_options[:ttl] ||= 4
|
42
|
+
@devices = []
|
43
|
+
@new_device_channel = EventMachine::Channel.new
|
44
|
+
@old_device_channel = EventMachine::Channel.new
|
45
|
+
end
|
46
|
+
|
47
|
+
# Starts the ControlPoint. If an EventMachine reactor is running already,
|
48
|
+
# it'll join that reactor, otherwise it'll start the reactor.
|
49
|
+
#
|
50
|
+
# @yieldparam [EventMachine::Channel] new_device_channel The means through
|
51
|
+
# which clients can get notified when a new device has been discovered
|
52
|
+
# either through SSDP searching or from an +ssdp:alive+ notification.
|
53
|
+
# @yieldparam [EventMachine::Channel] old_device_channel The means through
|
54
|
+
# which clients can get notified when a device has gone offline (have sent
|
55
|
+
# out a +ssdp:byebye+ notification).
|
56
|
+
def start(&blk)
|
57
|
+
@stopping = false
|
58
|
+
|
59
|
+
starter = -> do
|
60
|
+
ssdp_search_and_listen(@search_target, @search_options)
|
61
|
+
blk.call(@new_device_channel, @old_device_channel)
|
62
|
+
@running = true
|
63
|
+
end
|
64
|
+
|
65
|
+
if EM.reactor_running?
|
66
|
+
log 'Joining reactor...'
|
67
|
+
starter.call
|
68
|
+
else
|
69
|
+
log 'Starting reactor...'
|
70
|
+
EM.synchrony(&starter)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def listen(ttl)
|
75
|
+
EM.defer do
|
76
|
+
listener = SSDP.listen(ttl)
|
77
|
+
|
78
|
+
listener.alive_notifications.subscribe do |advertisement|
|
79
|
+
log "Got alive #{advertisement}"
|
80
|
+
|
81
|
+
if @devices.any? { |d| d.usn == advertisement[:usn] }
|
82
|
+
log "Device with USN #{advertisement[:usn]} already exists."
|
83
|
+
else
|
84
|
+
log "Device with USN #{advertisement[:usn]} not found. Creating..."
|
85
|
+
create_device(advertisement)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
listener.byebye_notifications.subscribe do |advertisement|
|
90
|
+
log "Got bye-bye from #{advertisement}"
|
91
|
+
|
92
|
+
@devices.reject! do |device|
|
93
|
+
device.usn == advertisement[:usn]
|
94
|
+
end
|
95
|
+
|
96
|
+
@old_device_channel << advertisement
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def ssdp_search_and_listen(search_for, options = {})
|
102
|
+
searcher = SSDP.search(search_for, options)
|
103
|
+
|
104
|
+
searcher.discovery_responses.subscribe do |notification|
|
105
|
+
create_device(notification)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Do I need to do this?
|
109
|
+
EM.add_timer(options[:response_wait_time]) do
|
110
|
+
searcher.close_connection
|
111
|
+
listen(options[:ttl])
|
112
|
+
end
|
113
|
+
|
114
|
+
EM.add_periodic_timer(5) do
|
115
|
+
log "Time since last timer: #{Time.now - @timer_time}" if @timer_time
|
116
|
+
log "Connections: #{EM.connection_count}"
|
117
|
+
@timer_time = Time.now
|
118
|
+
puts "<#{self.class}> Device count: #{@devices.size}"
|
119
|
+
puts "<#{self.class}> Device unique: #{@devices.uniq.size}"
|
120
|
+
end
|
121
|
+
|
122
|
+
trap_signals
|
123
|
+
end
|
124
|
+
|
125
|
+
def create_device(notification)
|
126
|
+
deferred_device = Device.new(ssdp_notification: notification)
|
127
|
+
|
128
|
+
deferred_device.errback do |partially_built_device, message|
|
129
|
+
log message
|
130
|
+
#add_device(partially_built_device)
|
131
|
+
|
132
|
+
if self.class.raise_on_remote_error
|
133
|
+
raise ControlPoint::Error, message
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
deferred_device.callback do |built_device|
|
138
|
+
log "Device created from #{notification}"
|
139
|
+
add_device(built_device)
|
140
|
+
end
|
141
|
+
|
142
|
+
deferred_device.fetch
|
143
|
+
end
|
144
|
+
|
145
|
+
def add_device(built_device)
|
146
|
+
if (@devices.any? { |d| d.usn == built_device.usn }) ||
|
147
|
+
(@devices.any? { |d| d.udn == built_device.udn })
|
148
|
+
log 'Newly created device already exists in internal list. Not adding.'
|
149
|
+
#if @devices.any? { |d| d.usn == built_device.usn }
|
150
|
+
# log "Newly created device (#{built_device.usn}) already exists in internal list. Not adding."
|
151
|
+
else
|
152
|
+
log 'Adding newly created device to internal list..'
|
153
|
+
@new_device_channel << built_device
|
154
|
+
@devices << built_device
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def stop
|
159
|
+
@running = false
|
160
|
+
@stopping = false
|
161
|
+
|
162
|
+
EM.stop if EM.reactor_running?
|
163
|
+
end
|
164
|
+
|
165
|
+
def running?
|
166
|
+
@running
|
167
|
+
end
|
168
|
+
|
169
|
+
def trap_signals
|
170
|
+
trap('INT') { stop }
|
171
|
+
trap('TERM') { stop }
|
172
|
+
trap('HUP') { stop } if RUBY_PLATFORM !~ /mswin|mingw/
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'nori'
|
2
|
+
require 'em-http-request'
|
3
|
+
require_relative 'error'
|
4
|
+
require_relative '../logger'
|
5
|
+
require_relative '../../playful'
|
6
|
+
|
7
|
+
|
8
|
+
module Playful
|
9
|
+
class ControlPoint
|
10
|
+
class Base
|
11
|
+
include LogSwitch::Mixin
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def get_description(location, description_getter)
|
16
|
+
log "Getting description with getter ID #{description_getter.object_id} for: #{location}"
|
17
|
+
http = EM::HttpRequest.new(location).aget
|
18
|
+
|
19
|
+
t = EM::Timer.new(30) do
|
20
|
+
http.fail(:timeout)
|
21
|
+
end
|
22
|
+
|
23
|
+
http.errback do |error|
|
24
|
+
if error == :timeout
|
25
|
+
log 'Timed out getting description. Retrying...'
|
26
|
+
http = EM::HttpRequest.new(location).get
|
27
|
+
else
|
28
|
+
log "Unable to retrieve DDF from #{location}", :error
|
29
|
+
log "Request error: #{http.error}"
|
30
|
+
log "Response status: #{http.response_header.status}"
|
31
|
+
|
32
|
+
description_getter.set_deferred_status(:failed)
|
33
|
+
|
34
|
+
if ControlPoint.raise_on_remote_error
|
35
|
+
raise ControlPoint::Error, "Unable to retrieve DDF from #{location}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
http.callback {
|
41
|
+
log "HTTP callback called for #{description_getter.object_id}"
|
42
|
+
response = xml_parser.parse(http.response)
|
43
|
+
description_getter.set_deferred_status(:succeeded, response)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def build_url(url_base, rest_of_url)
|
48
|
+
if url_base.end_with?('/') && rest_of_url.start_with?('/')
|
49
|
+
rest_of_url.sub!('/', '')
|
50
|
+
end
|
51
|
+
|
52
|
+
url_base + rest_of_url
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Nori::Parser]
|
56
|
+
def xml_parser
|
57
|
+
@xml_parser if @xml_parser
|
58
|
+
|
59
|
+
options = {
|
60
|
+
convert_tags_to: lambda { |tag| tag.to_sym }
|
61
|
+
}
|
62
|
+
|
63
|
+
begin
|
64
|
+
require 'nokogiri'
|
65
|
+
options.merge! parser: :nokogiri
|
66
|
+
rescue LoadError
|
67
|
+
warn "Tried loading nokogiri for XML couldn't. This is OK, just letting you know."
|
68
|
+
end
|
69
|
+
|
70
|
+
@xml_parser = Nori.new(options)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|