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
|
+
module Playful
|
2
|
+
class SSDP
|
3
|
+
module NetworkConstants
|
4
|
+
|
5
|
+
BROADCAST_IP = '255.255.255.255'
|
6
|
+
|
7
|
+
# Default multicast IP address
|
8
|
+
MULTICAST_IP = '239.255.255.250'
|
9
|
+
|
10
|
+
# Default multicast port
|
11
|
+
MULTICAST_PORT = 1900
|
12
|
+
|
13
|
+
# Default TTL
|
14
|
+
TTL = 4
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative '../logger'
|
2
|
+
require_relative 'multicast_connection'
|
3
|
+
|
4
|
+
|
5
|
+
class Playful::SSDP::Notifier < Playful::SSDP::MulticastConnection
|
6
|
+
include LogSwitch::Mixin
|
7
|
+
|
8
|
+
def initialize(nt, usn, ddf_url, valid_for_duration)
|
9
|
+
@os = RbConfig::CONFIG['host_vendor'].capitalize + '/' +
|
10
|
+
RbConfig::CONFIG['host_os']
|
11
|
+
@upnp_version = '1.0'
|
12
|
+
@notification = notification(nt, usn, ddf_url, valid_for_duration)
|
13
|
+
end
|
14
|
+
|
15
|
+
def post_init
|
16
|
+
if send_datagram(@notification, MULTICAST_IP, MULTICAST_PORT) > 0
|
17
|
+
log "Sent notification:\n#{@notification}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param [String] nt "Notification Type"; a potential search target. Used in
|
22
|
+
# +NT+ header.
|
23
|
+
# @param [String] usn "Unique Service Name"; a composite identifier for the
|
24
|
+
# advertisement. Used in +USN+ header.
|
25
|
+
# @param [String] ddf_url Device Description File URL for the root device.
|
26
|
+
# @param [Fixnum] valid_for_duration Duration in seconds for which the
|
27
|
+
# advertisement is valid. Used in +CACHE-CONTROL+ header.
|
28
|
+
def notification(nt, usn, ddf_url, valid_for_duration)
|
29
|
+
<<-NOTIFICATION
|
30
|
+
NOTIFY * HTTP/1.1\r
|
31
|
+
HOST: #{MULTICAST_IP}:#{MULTICAST_PORT}\r
|
32
|
+
CACHE-CONTROL: max-age=#{valid_for_duration}\r
|
33
|
+
LOCATION: #{ddf_url}\r
|
34
|
+
NT: #{nt}\r
|
35
|
+
NTS: ssdp:alive\r
|
36
|
+
SERVER: #{@os} UPnP/#{@upnp_version} Playful/#{Playful::VERSION}\r
|
37
|
+
USN: #{usn}\r
|
38
|
+
\r
|
39
|
+
NOTIFICATION
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative '../logger'
|
2
|
+
require_relative 'multicast_connection'
|
3
|
+
|
4
|
+
module Playful
|
5
|
+
|
6
|
+
# A subclass of an EventMachine::Connection, this handles doing M-SEARCHes.
|
7
|
+
#
|
8
|
+
# Search types:
|
9
|
+
# ssdp:all
|
10
|
+
# upnp:rootdevice
|
11
|
+
# uuid:[device-uuid]
|
12
|
+
# urn:schemas-upnp-org:device:[deviceType-version]
|
13
|
+
# urn:schemas-upnp-org:service:[serviceType-version]
|
14
|
+
# urn:[custom-schema]:device:[deviceType-version]
|
15
|
+
# urn:[custom-schema]:service:[serviceType-version]
|
16
|
+
class SSDP::Searcher < SSDP::MulticastConnection
|
17
|
+
include LogSwitch::Mixin
|
18
|
+
|
19
|
+
DEFAULT_RESPONSE_WAIT_TIME = 5
|
20
|
+
DEFAULT_M_SEARCH_COUNT = 2
|
21
|
+
|
22
|
+
# @return [EventMachine::Channel] Provides subscribers with responses from
|
23
|
+
# their search request.
|
24
|
+
attr_reader :discovery_responses
|
25
|
+
|
26
|
+
# @param [String] search_target
|
27
|
+
# @param [Hash] options
|
28
|
+
# @option options [Fixnum] response_wait_time
|
29
|
+
# @option options [Fixnum] ttl
|
30
|
+
# @option options [Fixnum] m_search_count The number of times to send the
|
31
|
+
# M-SEARCH. UPnP 1.0 suggests to send the request more than once.
|
32
|
+
def initialize(search_target, options = {})
|
33
|
+
options[:ttl] ||= TTL
|
34
|
+
options[:response_wait_time] ||= DEFAULT_RESPONSE_WAIT_TIME
|
35
|
+
@m_search_count = options[:m_search_count] ||= DEFAULT_M_SEARCH_COUNT
|
36
|
+
|
37
|
+
@search = m_search(search_target, options[:response_wait_time])
|
38
|
+
|
39
|
+
super options[:ttl]
|
40
|
+
end
|
41
|
+
|
42
|
+
# This is the callback called by EventMachine when it receives data on the
|
43
|
+
# socket that's been opened for this connection. In this case, the method
|
44
|
+
# parses the SSDP responses/notifications into Hashes and adds them to the
|
45
|
+
# appropriate EventMachine::Channel (provided as accessor methods). This
|
46
|
+
# effectively means that in each Channel, you get a Hash that represents
|
47
|
+
# the headers for each response/notification that comes in on the socket.
|
48
|
+
#
|
49
|
+
# @param [String] response The data received on this connection's socket.
|
50
|
+
def receive_data(response)
|
51
|
+
ip, port = peer_info
|
52
|
+
log "Response from #{ip}:#{port}:\n#{response}\n"
|
53
|
+
parsed_response = parse(response)
|
54
|
+
|
55
|
+
return if parsed_response.has_key? :nts
|
56
|
+
return if parsed_response[:man] &&
|
57
|
+
parsed_response[:man] =~ /ssdp:discover/
|
58
|
+
|
59
|
+
@discovery_responses << parsed_response
|
60
|
+
end
|
61
|
+
|
62
|
+
# Sends the M-SEARCH that was built during init. Logs what was sent if the
|
63
|
+
# send was successful.
|
64
|
+
def post_init
|
65
|
+
@m_search_count.times do
|
66
|
+
if send_datagram(@search, MULTICAST_IP, MULTICAST_PORT) > 0
|
67
|
+
log "Sent datagram search:\n#{@search}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Builds the M-SEARCH request string.
|
73
|
+
#
|
74
|
+
# @param [String] search_target
|
75
|
+
# @param [Fixnum] response_wait_time
|
76
|
+
def m_search(search_target, response_wait_time)
|
77
|
+
<<-MSEARCH
|
78
|
+
M-SEARCH * HTTP/1.1\r
|
79
|
+
HOST: #{MULTICAST_IP}:#{MULTICAST_PORT}\r
|
80
|
+
MAN: "ssdp:discover"\r
|
81
|
+
MX: #{response_wait_time}\r
|
82
|
+
ST: #{search_target}\r
|
83
|
+
\r
|
84
|
+
MSEARCH
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require_relative '../playful/control_point'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
|
6
|
+
# Middleware that allows your Rack app to keep tabs on devices that the
|
7
|
+
# {Playful::ControlPoint} has found. UPnP devices that match the +search_type+
|
8
|
+
# are discovered and added to the list, then removed as those devices send out
|
9
|
+
# +ssdp:byebye+ notifications. All of this depends on +EventMachine::Channel+s,
|
10
|
+
# and thus requires that an EventMachine reactor is running. If you don't
|
11
|
+
# have one running, the {Playful::ControlPoint} will start one for you.
|
12
|
+
#
|
13
|
+
# @example Control all root devices
|
14
|
+
#
|
15
|
+
# Thin::Server.start('0.0.0.0', 3000) do
|
16
|
+
# use Rack::UPnPControlPoint, search_type: :root
|
17
|
+
#
|
18
|
+
# map "/devices" do
|
19
|
+
# run lambda { |env|
|
20
|
+
# devices = env['upnp.devices']
|
21
|
+
# friendly_names = devices.map(&:friendly_name).join("\n")
|
22
|
+
# [200, {'Content-Type' => 'text/plain'}, [friendly_names]]
|
23
|
+
# }
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
class UPnPControlPoint
|
28
|
+
|
29
|
+
# @param [Rack::Builder] app Your Rack application.
|
30
|
+
# @param [Hash] options Options to pass to the Playful::SSDP::Searcher.
|
31
|
+
# @see Playful::SSDP::Searcher
|
32
|
+
def initialize(app, options = {})
|
33
|
+
@app = app
|
34
|
+
@devices = []
|
35
|
+
options[:search_type] ||= :root
|
36
|
+
EM.next_tick { start_control_point(options[:search_type], options) }
|
37
|
+
end
|
38
|
+
|
39
|
+
# Creates and starts the {Playful::ControlPoint}, then manages the list of
|
40
|
+
# devices using the +EventMachine::Channel+ objects yielded in.
|
41
|
+
#
|
42
|
+
# @param [Symbol,String] search_type The device(s) you want to search for
|
43
|
+
# and control.
|
44
|
+
# @param [Hash] options Options to pass to the Playful::SSDP::Searcher.
|
45
|
+
# @see Playful::SSDP::Searcher
|
46
|
+
def start_control_point(search_type, options)
|
47
|
+
@cp = ::Playful::ControlPoint.new(search_type, options)
|
48
|
+
|
49
|
+
@cp.start do |new_device_channel, old_device_channel|
|
50
|
+
new_device_channel.subscribe do |notification|
|
51
|
+
@devices << notification
|
52
|
+
end
|
53
|
+
|
54
|
+
old_device_channel.subscribe do |old_device|
|
55
|
+
@devices.reject! { |d| d.usn == old_device[:usn] }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
# Adds the whole list of devices to <tt>env['upnp.devices']</tt> so that
|
62
|
+
# that list can be accessed from within your app.
|
63
|
+
#
|
64
|
+
# @param [Hash] env The Rack environment.
|
65
|
+
def call(env)
|
66
|
+
env['upnp.devices'] = @devices
|
67
|
+
@app.call(env)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/playful.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path('../lib', __FILE__)
|
3
|
+
require 'playful/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'playful'
|
7
|
+
s.version = Playful::VERSION
|
8
|
+
s.author = 'turboladen'
|
9
|
+
s.email = 'steve.loveless@gmail.com'
|
10
|
+
s.homepage = 'http://github.com/turboladen/playful'
|
11
|
+
s.summary = 'Use me to build a UPnP app!'
|
12
|
+
s.description = %q{playful provides the tools you need to build an app that runs
|
13
|
+
in a UPnP environment.}
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.extra_rdoc_files = %w(History.rdoc README.rdoc)
|
19
|
+
s.require_paths = ['lib']
|
20
|
+
s.required_ruby_version = Gem::Requirement.new('>=1.9.1')
|
21
|
+
|
22
|
+
s.add_dependency 'eventmachine', '>=1.0.0'
|
23
|
+
s.add_dependency 'em-http-request', '>=1.0.2'
|
24
|
+
s.add_dependency 'em-synchrony'
|
25
|
+
s.add_dependency 'nori', '>=2.0.2'
|
26
|
+
s.add_dependency 'log_switch', '>=0.4.0'
|
27
|
+
s.add_dependency 'savon', '~>2.0'
|
28
|
+
|
29
|
+
s.add_development_dependency 'bundler'
|
30
|
+
s.add_development_dependency 'cucumber', '>=1.0.0'
|
31
|
+
s.add_development_dependency 'em-websocket', '>=0.3.6'
|
32
|
+
s.add_development_dependency 'rake'
|
33
|
+
s.add_development_dependency 'rspec', '>=3.0.0.beta'
|
34
|
+
s.add_development_dependency 'simplecov', '>=0.4.2'
|
35
|
+
s.add_development_dependency 'thin'
|
36
|
+
s.add_development_dependency 'thor', '>=0.1.6'
|
37
|
+
s.add_development_dependency 'yard', '>=0.7.0'
|
38
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
|
3
|
+
SimpleCov.start
|
4
|
+
|
5
|
+
require 'coveralls'
|
6
|
+
Coveralls.wear!
|
7
|
+
|
8
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
9
|
+
|
10
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |file| require file }
|
11
|
+
|
12
|
+
ENV['RUBY_UPNP_ENV'] = 'testing'
|
13
|
+
|
14
|
+
RSpec.configure do |config|
|
15
|
+
#config.raise_errors_for_deprecations!
|
16
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
SSDP_SEARCH_RESPONSES_PARSED = {
|
2
|
+
root_device1: {
|
3
|
+
cache_control: 'max-age=1200',
|
4
|
+
date: 'Mon, 26 Sep 2011 06:40:19 GMT',
|
5
|
+
location: 'http://192.168.10.3:5001/description/fetch',
|
6
|
+
server: 'Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1',
|
7
|
+
st: 'upnp:rootdevice',
|
8
|
+
ext: nil,
|
9
|
+
usn: 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d::upnp:rootdevice',
|
10
|
+
content_length: 0
|
11
|
+
},
|
12
|
+
root_device2: {
|
13
|
+
cache_control: 'max-age=1200',
|
14
|
+
date: 'Mon, 26 Sep 2011 06:40:20 GMT',
|
15
|
+
location: 'http://192.168.10.4:5001/description/fetch',
|
16
|
+
server: 'Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1',
|
17
|
+
st: 'upnp:rootdevice',
|
18
|
+
ext: nil,
|
19
|
+
usn: 'uuid:3c202906-992d-3f0f-b94c-90e1902a136e::upnp:rootdevice',
|
20
|
+
content_length: 0
|
21
|
+
},
|
22
|
+
media_server: {
|
23
|
+
cache_control: 'max-age=1200',
|
24
|
+
date: 'Mon, 26 Sep 2011 06:40:21 GMT',
|
25
|
+
location: 'http://192.168.10.3:5001/description/fetch',
|
26
|
+
server: 'Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1',
|
27
|
+
st: 'urn:schemas-upnp-org:device:MediaServer:1',
|
28
|
+
ext: nil,
|
29
|
+
usn: 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d::urn:schemas-upnp-org:device:MediaServer:1',
|
30
|
+
content_length: 0
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
SSDP_DESCRIPTIONS = {
|
35
|
+
root_device1: {
|
36
|
+
'root' => {
|
37
|
+
'specVersion' => {
|
38
|
+
'major' => '1',
|
39
|
+
'minor' => '0'
|
40
|
+
},
|
41
|
+
'URLBase' => 'http://192.168.10.3:5001/',
|
42
|
+
'device' => {
|
43
|
+
'dlna:X_DLNADOC' => %w[DMS-1.50 M-DMS-1.50],
|
44
|
+
'deviceType' => 'urn:schemas-upnp-org:device:MediaServer:1',
|
45
|
+
'friendlyName' => 'PS3 Media Server [gutenberg]',
|
46
|
+
'manufacturer' => 'PMS',
|
47
|
+
'manufacturerURL' => 'http://ps3mediaserver.blogspot.com',
|
48
|
+
'modelDescription' => 'UPnP/AV 1.0 Compliant Media Server',
|
49
|
+
'modelName' => 'PMS',
|
50
|
+
'modelNumber' => '01',
|
51
|
+
'modelURL' => 'http://ps3mediaserver.blogspot.com',
|
52
|
+
'serialNumber' => nil,
|
53
|
+
'UPC' => nil,
|
54
|
+
'UDN' => 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d',
|
55
|
+
'iconList' => {
|
56
|
+
'icon' => {
|
57
|
+
'mimetype' => 'image/jpeg',
|
58
|
+
'width' => '120',
|
59
|
+
'height' => '120',
|
60
|
+
'depth' => '24',
|
61
|
+
'url' => '/images/icon-256.png'
|
62
|
+
}
|
63
|
+
},
|
64
|
+
'presentationURL' => 'http://192.168.10.3:5001/console/index.html',
|
65
|
+
'serviceList' => {
|
66
|
+
'service' => [
|
67
|
+
{
|
68
|
+
'serviceType' => 'urn:schemas-upnp-org:service:ContentDirectory:1',
|
69
|
+
'serviceId' => 'urn:upnp-org:serviceId:ContentDirectory',
|
70
|
+
'SCPDURL' => '/UPnP_AV_ContentDirectory_1.0.xml',
|
71
|
+
'controlURL' => '/upnp/control/content_directory',
|
72
|
+
'eventSubURL' => '/upnp/event/content_directory'
|
73
|
+
},
|
74
|
+
{
|
75
|
+
'serviceType' => 'urn:schemas-upnp-org:service:ConnectionManager:1',
|
76
|
+
'serviceId' => 'urn:upnp-org:serviceId:ConnectionManager',
|
77
|
+
'SCPDURL' => '/UPnP_AV_ConnectionManager_1.0.xml',
|
78
|
+
'controlURL' => '/upnp/control/connection_manager',
|
79
|
+
'eventSubURL' => '/upnp/event/connection_manager'
|
80
|
+
}
|
81
|
+
]
|
82
|
+
}
|
83
|
+
},
|
84
|
+
'@xmlns:dlna' => 'urn:schemas-dlna-org:device-1-0',
|
85
|
+
'@xmlns' => 'urn:schemas-upnp-org:device-1-0'
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
ROOT_DEVICE1 = <<-RD
|
91
|
+
HTTP/1.1 200 OK
|
92
|
+
CACHE-CONTROL: max-age=1200
|
93
|
+
DATE: Mon, 26 Sep 2011 06:40:19 GMT
|
94
|
+
LOCATION: http://1.2.3.4:5678/description/fetch
|
95
|
+
SERVER: Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1
|
96
|
+
ST: upnp:rootdevice
|
97
|
+
EXT:
|
98
|
+
USN: uuid:3c202906-992d-3f0f-b94c-90e1902a136d::upnp:rootdevice
|
99
|
+
Content-Length: 0
|
100
|
+
|
101
|
+
RD
|
102
|
+
|
103
|
+
ROOT_DEVICE2 = <<-RD
|
104
|
+
HTTP/1.1 200 OK
|
105
|
+
CACHE-CONTROL: max-age=1200
|
106
|
+
DATE: Mon, 26 Sep 2011 06:40:20 GMT
|
107
|
+
LOCATION: http://1.2.3.4:5678/description/fetch
|
108
|
+
SERVER: Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1
|
109
|
+
ST: upnp:rootdevice
|
110
|
+
EXT:
|
111
|
+
USN: uuid:3c202906-992d-3f0f-b94c-90e1902a136e::upnp:rootdevice
|
112
|
+
Content-Length: 0
|
113
|
+
|
114
|
+
RD
|
115
|
+
|
116
|
+
MEDIA_SERVER = <<-MD
|
117
|
+
HTTP/1.1 200 OK
|
118
|
+
CACHE-CONTROL: max-age=1200
|
119
|
+
DATE: Mon, 26 Sep 2011 06:40:21 GMT
|
120
|
+
LOCATION: http://1.2.3.4:5678/description/fetch
|
121
|
+
SERVER: Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1
|
122
|
+
ST: urn:schemas-upnp-org:device:MediaServer:1
|
123
|
+
EXT:
|
124
|
+
USN: uuid:3c202906-992d-3f0f-b94c-90e1902a136d::urn:schemas-upnp-org:device:MediaServer:
|
125
|
+
Content-Length: 0
|
126
|
+
|
127
|
+
MD
|
128
|
+
|
129
|
+
RESPONSES = {
|
130
|
+
root_device1: ROOT_DEVICE1,
|
131
|
+
root_device2: ROOT_DEVICE2,
|
132
|
+
media_server: MEDIA_SERVER
|
133
|
+
}
|
134
|
+
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'core_ext/to_upnp_s'
|
3
|
+
|
4
|
+
|
5
|
+
describe Hash do
|
6
|
+
describe '#to_upnp_s' do
|
7
|
+
context ':uuid as key' do
|
8
|
+
it "returns a String like 'uuid:[Hash value]'" do
|
9
|
+
expect({ uuid: '12345' }.to_upnp_s).to eq 'uuid:12345'
|
10
|
+
end
|
11
|
+
|
12
|
+
it "doesn't check if the Hash value is legit" do
|
13
|
+
expect({ uuid: '' }.to_upnp_s).to eq 'uuid:'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context ':device_type as key' do
|
18
|
+
context 'domain name not given' do
|
19
|
+
it "returns a String like 'urn:schemas-upnp-org:device:[device type]'" do
|
20
|
+
expect({ device_type: '12345' }.to_upnp_s).
|
21
|
+
to eq 'urn:schemas-upnp-org:device:12345'
|
22
|
+
end
|
23
|
+
|
24
|
+
it "doesn't check if the Hash value is legit" do
|
25
|
+
expect({ device_type: '' }.to_upnp_s).to eq 'urn:schemas-upnp-org:device:'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'domain name given' do
|
30
|
+
it "returns a String like 'urn:[domain name]:device:[device type]'" do
|
31
|
+
expect({ device_type: '12345', domain_name: 'my-domain' }.to_upnp_s).
|
32
|
+
to eq 'urn:my-domain:device:12345'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "doesn't check if the Hash value is legit" do
|
36
|
+
expect({ device_type: '', domain_name: 'stuff' }.to_upnp_s).
|
37
|
+
to eq 'urn:stuff:device:'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context ':service_type as key' do
|
43
|
+
context 'domain name not given' do
|
44
|
+
it "returns a String like 'urn:schemas-upnp-org:service:[service type]'" do
|
45
|
+
expect({ service_type: '12345' }.to_upnp_s).
|
46
|
+
to eq 'urn:schemas-upnp-org:service:12345'
|
47
|
+
end
|
48
|
+
|
49
|
+
it "doesn't check if the Hash value is legit" do
|
50
|
+
expect({ service_type: '' }.to_upnp_s).to eq 'urn:schemas-upnp-org:service:'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'domain name given' do
|
55
|
+
it "returns a String like 'urn:[domain name]:service:[service type]'" do
|
56
|
+
expect({ service_type: '12345', domain_name: 'my-domain' }.to_upnp_s).
|
57
|
+
to eq 'urn:my-domain:service:12345'
|
58
|
+
end
|
59
|
+
|
60
|
+
it "doesn't check if the Hash value is legit" do
|
61
|
+
expect({ service_type: '', domain_name: 'my-domain' }.to_upnp_s).
|
62
|
+
to eq 'urn:my-domain:service:'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'some other Hash key' do
|
68
|
+
context 'domain name not given' do
|
69
|
+
it 'returns self.to_s' do
|
70
|
+
expect({ firestorm: 12345 }.to_upnp_s).to eq '{:firestorm=>12345}'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe Symbol do
|
78
|
+
context ':all' do
|
79
|
+
describe '#to_upnp_s' do
|
80
|
+
it "returns 'ssdp:all'" do
|
81
|
+
expect(:all.to_upnp_s).to eq 'ssdp:all'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context ':root' do
|
87
|
+
describe '#to_upnp_s' do
|
88
|
+
it "returns 'upnp:rootdevice'" do
|
89
|
+
expect(:root.to_upnp_s).to eq 'upnp:rootdevice'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it "returns itself if one of the defined shortcuts wasn't given" do
|
95
|
+
expect(:firestorm.to_upnp_s).to eq :firestorm
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe String do
|
100
|
+
it 'returns itself' do
|
101
|
+
expect('Stuff and things'.to_upnp_s).to eq 'Stuff and things'
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|