airplay 1.0.2 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,60 @@
1
+ module Airplay
2
+ module CLI
3
+ class Doctor
4
+ DebugDevice = Struct.new(:node, :resolved) do
5
+ def host
6
+ info = Socket.getaddrinfo(resolved.target, nil, Socket::AF_INET)
7
+ info[0][2]
8
+ rescue SocketError
9
+ target
10
+ end
11
+ end
12
+
13
+ attr_accessor :devices
14
+
15
+ def initialize
16
+ @devices = []
17
+ end
18
+
19
+ def information
20
+ find_devices!
21
+
22
+ devices.each do |device|
23
+ puts <<-EOS.gsub!(" "*12, "")
24
+ Name: #{device.node.name}
25
+ Host: #{device.host}
26
+ Port: #{device.resolved.port}
27
+ Full Name: #{device.node.fullname}
28
+ Iface: #{device.node.interface_name}
29
+ TXT: #{device.resolved.text_record}
30
+
31
+ EOS
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def find_devices!
38
+ timeout(5) do
39
+ DNSSD.browse!(Airplay::Browser::SEARCH) do |node|
40
+ try_resolving(node)
41
+ break unless node.flags.more_coming?
42
+ end
43
+ end
44
+ end
45
+
46
+ def try_resolving(node)
47
+ timeout(5) do
48
+ resolver = DNSSD::Service.new
49
+ resolver.resolve(node) do |resolved|
50
+ devices << DebugDevice.new(node, resolved)
51
+
52
+ break unless resolved.flags.more_coming?
53
+ end
54
+ end
55
+ end
56
+
57
+
58
+ end
59
+ end
60
+ end
@@ -1,5 +1,5 @@
1
1
  module Airplay
2
2
  module CLI
3
- VERSION = "1.0.1"
3
+ VERSION = "1.0.2"
4
4
  end
5
5
  end
@@ -1,4 +1,5 @@
1
1
  require "log4r/config"
2
+ require "celluloid/autostart"
2
3
  require "airplay/logger"
3
4
 
4
5
  # Public: Airplay core module
@@ -10,12 +11,13 @@ module Airplay
10
11
  attr_accessor :log_level, :output, :autodiscover, :host, :port
11
12
 
12
13
  def initialize
14
+ Celluloid.boot # Force Thread Pool initialization
13
15
  Log4r.define_levels(*Log4r::Log4rConfig::LogLevels)
14
16
 
15
17
  @log_level = Log4r::ERROR
16
18
  @autodiscover = true
17
19
  @host = "0.0.0.0"
18
- @port = "1337"
20
+ @port = nil
19
21
  @output = Log4r::Outputter.stdout
20
22
  end
21
23
 
@@ -1,4 +1,4 @@
1
- require "celluloid"
1
+ require "celluloid/autostart"
2
2
  require "airplay/connection/persistent"
3
3
  require "airplay/connection/authentication"
4
4
 
@@ -7,6 +7,8 @@ module Airplay
7
7
  #
8
8
  class Connection
9
9
  Response = Struct.new(:connection, :response)
10
+ PasswordRequired = Class.new(StandardError)
11
+ WrongPassword = Class.new(StandardError)
10
12
 
11
13
  include Celluloid
12
14
 
@@ -43,11 +45,7 @@ module Airplay
43
45
  # Returns a response object
44
46
  #
45
47
  def post(resource, body = "", headers = {})
46
- @logger.info("POST #{resource} with #{body.bytesize} bytes")
47
- request = Net::HTTP::Post.new(resource)
48
- request.body = body
49
-
50
- send_request(request, headers)
48
+ prepare_request(:post, resource, body, headers)
51
49
  end
52
50
 
53
51
  # Public: Executes a PUT to a resource
@@ -59,11 +57,7 @@ module Airplay
59
57
  # Returns a response object
60
58
  #
61
59
  def put(resource, body = "", headers = {})
62
- @logger.info("PUT #{resource} with #{body.bytesize} bytes")
63
- request = Net::HTTP::Put.new(resource)
64
- request.body = body
65
-
66
- send_request(request, headers)
60
+ prepare_request(:put, resource, body, headers)
67
61
  end
68
62
 
69
63
  # Public: Executes a GET to a resource
@@ -74,14 +68,34 @@ module Airplay
74
68
  # Returns a response object
75
69
  #
76
70
  def get(resource, headers = {})
77
- @logger.info("GET #{resource}")
78
- request = Net::HTTP::Get.new(resource)
79
-
80
- send_request(request, headers)
71
+ prepare_request(:get, resource, nil, headers)
81
72
  end
82
73
 
83
74
  private
84
75
 
76
+ # Private: Prepares HTTP requests for :get, :post and :put
77
+ #
78
+ # verb - The http method/verb to use for the request
79
+ # resource - The resource on the currently active Device
80
+ # body - The body of the action
81
+ # headers - The headers of the request
82
+ #
83
+ # Returns a response object
84
+ #
85
+ def prepare_request(verb, resource, body, headers)
86
+ msg = "#{verb.upcase} #{resource}"
87
+
88
+ request = Net::HTTP.const_get(verb.capitalize).new(resource)
89
+
90
+ unless verb.eql?(:get)
91
+ request.body = body
92
+ msg.concat(" with #{body.bytesize} bytes")
93
+ end
94
+
95
+ @logger.info(msg)
96
+ send_request(request, headers)
97
+ end
98
+
85
99
  # Private: The defaults connection headers
86
100
  #
87
101
  # Returns the default headers
@@ -105,14 +119,29 @@ module Airplay
105
119
  request.initialize_http_header(default_headers.merge(headers))
106
120
 
107
121
  if @device.password?
108
- authentication = Airplay::Connection::Authentication.new(persistent)
122
+ authentication = Airplay::Connection::Authentication.new(@device, persistent)
109
123
  request = authentication.sign(request)
110
124
  end
111
125
 
112
126
  @logger.info("Sending request to #{@device.address}")
113
127
  response = persistent.request(request)
114
128
 
115
- Airplay::Connection::Response.new(persistent, response)
129
+ verify_response(Airplay::Connection::Response.new(persistent, response))
130
+ end
131
+
132
+ # Private: Verifies response
133
+ #
134
+ # response - The Response object
135
+ #
136
+ # Returns a response object or exception
137
+ #
138
+ def verify_response(response)
139
+ if response.response.status == 401
140
+ return PasswordRequired.new if !@device.password?
141
+ return WrongPassword.new if @device.password?
142
+ end
143
+
144
+ response
116
145
  end
117
146
  end
118
147
  end
@@ -2,7 +2,7 @@ require "net/http/digest_auth"
2
2
 
3
3
  module Airplay
4
4
  class Connection
5
- class Authentication < Struct.new(:handler)
5
+ Authentication = Struct.new(:device, :handler) do
6
6
  def sign(request)
7
7
  auth_token = authenticate(request)
8
8
  request.add_field('Authorization', auth_token) if auth_token
@@ -12,11 +12,10 @@ module Airplay
12
12
  private
13
13
 
14
14
  def uri(request)
15
- server = Airplay.active
16
- path = "http://#{server.address}#{request.path}"
15
+ path = "http://#{device.address}#{request.path}"
17
16
  uri = URI.parse(path)
18
17
  uri.user = "Airplay"
19
- uri.password = server.password
18
+ uri.password = device.password
20
19
 
21
20
  uri
22
21
  end
@@ -23,6 +23,8 @@ module Airplay
23
23
  @type = attributes[:type]
24
24
  @password = attributes[:password]
25
25
 
26
+ @it_has_password = false
27
+
26
28
  Airplay.configuration.load
27
29
  end
28
30
 
@@ -34,6 +36,19 @@ module Airplay
34
36
  @_ip ||= address.split(":").first
35
37
  end
36
38
 
39
+ # Public: Sets server information based on text records
40
+ #
41
+ # Returns text records hash.
42
+ #
43
+ def text_records=(record)
44
+ @text_records = {
45
+ "model" => record["model"],
46
+ "features" => record["features"],
47
+ "macAddress" => record["deviceid"],
48
+ "srcvers" => record["srcvers"]
49
+ }
50
+ end
51
+
37
52
  # Public: Sets the password for the device
38
53
  #
39
54
  # passwd - The password string
@@ -49,6 +64,7 @@ module Airplay
49
64
  # Returns boolean for the presence of a password
50
65
  #
51
66
  def password?
67
+ return @it_has_password if @it_has_password
52
68
  !!password && !password.empty?
53
69
  end
54
70
 
@@ -102,26 +118,30 @@ module Airplay
102
118
  @_connection = nil
103
119
  end
104
120
 
105
- private
106
-
107
- # Private: Validates the mandatory attributes for a device
121
+ # Public: The unique id of the device (mac address)
108
122
  #
109
- # attributes - The attributes hash to be validated
123
+ # Returns the mac address based on basic_info or server_info
110
124
  #
111
- # Returns nothing or raises a MissingAttributes if some key is missing
112
- #
113
- def validate_attributes(attributes)
114
- if !([:name, :address] - attributes.keys).empty?
115
- raise MissingAttributes.new("A :name and an :address are mandatory")
125
+ def id
126
+ @_id ||= begin
127
+ basic_info.fetch("macAddress", server_info["macAddress"])
116
128
  end
117
129
  end
118
130
 
131
+ private
132
+
133
+ def it_has_password!
134
+ @it_has_password = true
135
+ end
136
+
119
137
  # Private: Access the basic info of the device
120
138
  #
121
139
  # Returns a hash with the basic information
122
140
  #
123
141
  def basic_info
124
142
  @_basic_info ||= begin
143
+ return @text_records if @text_records
144
+
125
145
  response = connection.get("/server-info").response
126
146
  plist = CFPropertyList::List.new(data: response.body)
127
147
  CFPropertyList.native_types(plist.value)
@@ -133,16 +153,36 @@ module Airplay
133
153
  # Returns a hash with extra information
134
154
  #
135
155
  def extra_info
136
- @_extra_info ||= begin
137
- new_device = clone
138
- new_device.refresh_connection
139
- new_device.address = "#{ip}:7100"
156
+ @_extra_info ||=
157
+ begin
158
+ new_device = clone
159
+ new_device.refresh_connection
160
+ new_device.address = "#{ip}:7100"
140
161
 
141
- response = new_device.connection.get("/stream.xml").response
142
- return {} if response.status != 200
162
+ result = new_device.connection.get("/stream.xml")
163
+ raise result if !result.is_a?(Airplay::Connection::Response)
143
164
 
144
- plist = CFPropertyList::List.new(data: response.body)
145
- CFPropertyList.native_types(plist.value)
165
+ response = result.response
166
+ return {} if response.status != 200
167
+
168
+ plist = CFPropertyList::List.new(data: response.body)
169
+ CFPropertyList.native_types(plist.value)
170
+ rescue Airplay::Connection::PasswordRequired
171
+ it_has_password!
172
+
173
+ return {}
174
+ end
175
+ end
176
+
177
+ # Private: Validates the mandatory attributes for a device
178
+ #
179
+ # attributes - The attributes hash to be validated
180
+ #
181
+ # Returns nothing or raises a MissingAttributes if some key is missing
182
+ #
183
+ def validate_attributes(attributes)
184
+ if !([:name, :address] - attributes.keys).empty?
185
+ raise MissingAttributes.new("A :name and an :address are mandatory")
146
186
  end
147
187
  end
148
188
  end
@@ -10,7 +10,7 @@ module Airplay
10
10
  def initialize(device)
11
11
  @device = device
12
12
  @model = device.server_info["model"]
13
- @os_version = device.server_info["osBuildVersion"]
13
+ @os_version = device.server_info["srcvers"]
14
14
  @mac_address = device.server_info["macAddress"]
15
15
  end
16
16
 
@@ -42,7 +42,9 @@ module Airplay
42
42
  # Returns nothing
43
43
  #
44
44
  def add(name, address)
45
- self << Device.new(name: name, address: address)
45
+ device = Device.new(name: name, address: address)
46
+ self << device
47
+ device
46
48
  end
47
49
 
48
50
  # Public: Adds a device to the list
@@ -1,7 +1,7 @@
1
1
  require "uri"
2
2
  require "forwardable"
3
3
  require "micromachine"
4
- require "celluloid"
4
+ require "celluloid/autostart"
5
5
  require "cfpropertylist"
6
6
 
7
7
  require "airplay/connection"
@@ -130,7 +130,7 @@ module Airplay
130
130
  #
131
131
  def scrub
132
132
  return unless playing?
133
- response = connection.get("/scrub")
133
+ response = connection.get("/scrub").response
134
134
  parts = response.body.split("\n")
135
135
  Hash[parts.collect { |v| v.split(": ") }]
136
136
  end
@@ -170,6 +170,14 @@ module Airplay
170
170
  connection.post("/stop")
171
171
  end
172
172
 
173
+ # Public: Seeks to the specified position (seconds) in the video
174
+ #
175
+ # Returns nothing
176
+ #
177
+ def seek(position)
178
+ connection.async.post("/scrub?position=#{position}")
179
+ end
180
+
173
181
  def loading?; state == :loading end
174
182
  def playing?; state == :playing end
175
183
  def paused?; state == :paused end
@@ -1,6 +1,6 @@
1
1
  require "rack"
2
2
  require "socket"
3
- require "celluloid"
3
+ require "celluloid/autostart"
4
4
  require "reel/rack"
5
5
 
6
6
  require "airplay/logger"
@@ -10,8 +10,10 @@ module Airplay
10
10
  class Server
11
11
  include Celluloid
12
12
 
13
+ attr_reader :port
14
+
13
15
  def initialize
14
- @port = Airplay.configuration.port
16
+ @port = Airplay.configuration.port || find_free_port
15
17
  @logger = Airplay::Logger.new("airplay::server")
16
18
  @server = Rack::Server.new(
17
19
  server: :reel,
@@ -53,9 +55,9 @@ module Airplay
53
55
  #
54
56
  # Returns a boolean with the state
55
57
  #
56
- def running?
58
+ def running?(port = @port)
57
59
  begin
58
- socket = TCPSocket.new(private_ip, @port)
60
+ socket = TCPSocket.new(private_ip, port)
59
61
  socket.close unless socket.nil?
60
62
  true
61
63
  rescue Errno::ECONNREFUSED, Errno::EBADF, Errno::EADDRNOTAVAIL
@@ -72,5 +74,17 @@ module Airplay
72
74
  addr.ipv4_private?
73
75
  end.ip_address
74
76
  end
77
+
78
+ # Private: Finds a free port by asking the kernel for a free one
79
+ #
80
+ # Returns a free port number
81
+ #
82
+ def find_free_port
83
+ socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
84
+ socket.listen(1)
85
+ port = socket.local_address.ip_port
86
+ socket.close
87
+ port
88
+ end
75
89
  end
76
90
  end
@@ -1,3 +1,3 @@
1
1
  module Airplay
2
- VERSION = "1.0.2"
2
+ VERSION = "1.0.3"
3
3
  end