Module: Sonos::Endpoint::AVTransport

Included in:
Device::Speaker
Defined in:
lib/sonos/endpoint/a_v_transport.rb

Constant Summary collapse

TRANSPORT_ENDPOINT =
'/MediaRenderer/AVTransport/Control'
TRANSPORT_XMLNS =
'urn:schemas-upnp-org:service:AVTransport:1'

Instance Method Summary collapse

Instance Method Details

#add_rdio_to_queue(opts = {}, position = 0) ⇒ Object

Add an Rdio object to the queue (album or track), anything else can only be streamed (play now). @param opts Various options (album/track keys, username and type) @param position Optional queue insertion position. Leaving this blank inserts at the end. @return Queue position of the added track(s)



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/sonos/endpoint/a_v_transport.rb', line 177

def add_rdio_to_queue(opts = {}, position = 0)
  opts = {
    :username => nil,
    :album    => nil,
    :track    => nil,
    :type     => 'track',
    :format   => 'mp3' # can be changed, but only 'mp3' is valid.
  }.merge(opts)

  return nil if opts[:username].nil?

  # Both tracks and albums require the album key. And tracks need a track
  # key of course.
  return nil if opts[:album].nil?
  return nil if opts[:type] == 'track' and opts[:track].nil?

  # In order for valid DIDL we'll pass an empty :track for albums.
  opts[:track] = '' if opts[:type] == 'album'

   = "<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="00030020_t%3a%3a#{opts[:track]}%3a%3aa%3a%3a#{opts[:album]}" parentID="0004006c_a%3a%3a#{opts[:album]}" restricted="true"><dc:title></dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class><desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">SA_RINCON2823_#{opts[:username]}</desc></item></DIDL-Lite>"

  case opts[:type]
  when /track/
    uri = "x-sonos-http:_t%3a%3a#{opts[:track]}%3a%3aa%3a%3a#{opts[:album]}.#{opts[:format]}?sid=11&flags=32"
  when /album/
    type_id = '0004006c_a'
    uri = "x-rincon-cpcontainer:#{type_id}%3a%3a#{opts[:album]}"
  else
    return nil
  end

  add_to_queue(uri, , position)
end

#add_spotify_to_queue(opts = {}, position = 0) ⇒ Object

Adds a Spotify track to the queue along with extra data for better metadata retrieval @param opts Various options (id, user, region and type) @param position Optional queue insertion position. Leaving this blank inserts at the end. @return Queue position of the added track(s)



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/sonos/endpoint/a_v_transport.rb', line 132

def add_spotify_to_queue(opts = {}, position = 0)
 opts = {
   :id     => '',
   :user   => nil,
   :region => nil,
   :type   => 'track'
 }.merge(opts)

 # Basic validation of the accepted types; playlists need an associated user
 # and the toplist (for tracks and albums) need to specify a region.
 return nil if opts[:type] == 'playlist' and opts[:user].nil?
 return nil if opts[:type] =~ /toplist_tracks/ and opts[:region].nil?

 # In order for the player to retrieve track duration, artist, album etc
 # we need to pass it some metadata ourselves.
  = "<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="#{rand(10000000..99999999)}spotify%3a#{opts[:type]}%3a#{opts[:id]}" restricted="true"><dc:title></dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class><desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">SA_RINCON2311_X_#Svc2311-0-Token</desc></item></DIDL-Lite>"

 r_id = rand(10000000..99999999)

 case opts[:type]
 when /playlist/
   uri = "x-rincon-cpcontainer:#{r_id}spotify%3auser%3a#{opts[:user]}%3aplaylist%3a#{opts[:id]}"
 when /toplist_(tracks)/
   subtype = opts[:type].sub('toplist_', '') # only 'tracks' are supported right now by Sonos.
   uri = "x-rincon-cpcontainer:#{r_id}toplist%2f#{subtype}%2fregion%2f#{opts[:region]}"
 when /album/
   uri = "x-rincon-cpcontainer:#{r_id}spotify%3aalbum%3a#{opts[:id]}"
 when /artist/
   uri = "x-rincon-cpcontainer:#{r_id}tophits%3aspotify%3aartist%3a#{opts[:id]}"
 when /starred/
   uri = "x-rincon-cpcontainer:#{r_id}starred"
 when /track/
   uri = "x-sonos-spotify:spotify%3a#{opts[:type]}%3a#{opts[:id]}"
 else
   return nil
 end

 add_to_queue(uri, , position)
end

#add_to_queue(uri, didl = '', position = 0) ⇒ Object

Adds a track to the queue @param uri Uri of track @param didl Stanza of DIDL-Lite metadata (generally created by #add_spotify_to_queue) @param position Optional queue insertion position. Leaving this blank inserts at the end. @return Queue position of the added track



119
120
121
122
123
124
125
126
# File 'lib/sonos/endpoint/a_v_transport.rb', line 119

def add_to_queue(uri, didl = '', position = 0)
  response = send_transport_message('AddURIToQueue', "<EnqueuedURI>#{uri}</EnqueuedURI><EnqueuedURIMetaData>#{didl}</EnqueuedURIMetaData><DesiredFirstTrackNumberEnqueued>#{position}</DesiredFirstTrackNumberEnqueued><EnqueueAsNext>1</EnqueueAsNext>")
  # TODO yeah, this error handling is a bit soft. For consistency's sake :)
  pos = response.xpath('.//FirstTrackNumberEnqueued').text
  if pos.length != 0
    pos.to_i
  end
end

#clear_queueObject

Clear the queue



105
106
107
# File 'lib/sonos/endpoint/a_v_transport.rb', line 105

def clear_queue
  parse_response send_transport_message('RemoveAllTracksFromQueue')
end

#get_player_stateObject

Get information about the state the player is in.



40
41
42
43
44
45
46
47
48
49
# File 'lib/sonos/endpoint/a_v_transport.rb', line 40

def get_player_state
  response = send_transport_message('GetTransportInfo')
  body = response.body[:get_transport_info_response]

  {
    status: body[:current_transport_status],
    state:  body[:current_transport_state],
    speed:  body[:current_speed],
  }
end

#group(slave) ⇒ Object

Add another speaker to this group. Trying to call this on a stereo pair slave will fail.



225
226
227
# File 'lib/sonos/endpoint/a_v_transport.rb', line 225

def group(slave)
  slave.join(self)
end

#has_music?Boolean

Returns:

  • (Boolean)


35
36
37
# File 'lib/sonos/endpoint/a_v_transport.rb', line 35

def has_music?
  !now_playing.nil?
end

#is_playing?Boolean

Returns true if the player is not in a paused or stopped state

Returns:

  • (Boolean)


52
53
54
55
# File 'lib/sonos/endpoint/a_v_transport.rb', line 52

def is_playing?
  state = get_player_state[:state]
  !['PAUSED_PLAYBACK', 'STOPPED'].include?(state)
end

#join(master) ⇒ Object

Join another speaker’s group. Trying to call this on a stereo pair slave will fail.



219
220
221
# File 'lib/sonos/endpoint/a_v_transport.rb', line 219

def join(master)
  parse_response set_av_transport_uri('x-rincon:' + master.uid.sub('uuid:', ''))
end

#line_in(speaker) ⇒ Object



87
88
89
# File 'lib/sonos/endpoint/a_v_transport.rb', line 87

def line_in(speaker)
  set_av_transport_uri('x-rincon-stream:' + speaker.uid.sub('uuid:', ''))
end

#nextObject

Play the next track.



78
79
80
# File 'lib/sonos/endpoint/a_v_transport.rb', line 78

def next
  parse_response send_transport_message('Next')
end

#now_playingHash

Get information about the currently playing track.

Returns:

  • (Hash)

    information about the current track.



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/sonos/endpoint/a_v_transport.rb', line 9

def now_playing
  response = send_transport_message('GetPositionInfo')
  body = response.body[:get_position_info_response]
  doc = Nokogiri::XML(body[:track_meta_data])

  # No music
  return nil if doc.children.length == 0

  art_path = doc.xpath('//upnp:albumArtURI').inner_text

  # TODO: No idea why this is necessary. Maybe its a Nokogiri thing
  art_path.sub!('/getaa?s=1=x-sonos-http', '/getaa?s=1&u=x-sonos-http')

  {
    title: doc.xpath('//dc:title').inner_text,
    artist: doc.xpath('//dc:creator').inner_text,
    album: doc.xpath('//upnp:album').inner_text,
    info: doc.xpath('//r:streamContent').inner_text,
    queue_position: body[:track],
    track_duration: body[:track_duration],
    current_position: body[:rel_time],
    uri: body[:track_uri],
    album_art: "http://#{self.ip}:#{Sonos::PORT}#{art_path}"
  }
end

#pauseObject

Pause the currently playing track.



58
59
60
# File 'lib/sonos/endpoint/a_v_transport.rb', line 58

def pause
  parse_response send_transport_message('Pause')
end

#play(uri = nil) ⇒ Object

Play the currently selected track or play a stream.

Parameters:

  • uri (String) (defaults to: nil)

    Optional uri of the track to play. Leaving this blank, plays the current track.



64
65
66
67
68
69
70
# File 'lib/sonos/endpoint/a_v_transport.rb', line 64

def play(uri = nil)
  # Play a song from the uri
  set_av_transport_uri(uri) and return if uri

  # Play the currently selected track
  parse_response send_transport_message('Play')
end

#previousObject

Play the previous track.



83
84
85
# File 'lib/sonos/endpoint/a_v_transport.rb', line 83

def previous
  parse_response send_transport_message('Previous')
end

#remove_from_queue(object_id) ⇒ Object

Removes a track from the queue @param object_id Track’s queue ID



213
214
215
# File 'lib/sonos/endpoint/a_v_transport.rb', line 213

def remove_from_queue(object_id)
  parse_response send_transport_message('RemoveTrackFromQueue', "<ObjectID>#{object_id}</ObjectID><UpdateID>0</UpdateID></u:RemoveTrackFromQueue>")
end

#save_queue(title) ⇒ Object

Save queue



110
111
112
# File 'lib/sonos/endpoint/a_v_transport.rb', line 110

def save_queue(title)
  parse_response send_transport_message('SaveQueue', "<Title>#{title}</Title><ObjectID></ObjectID>")
end

#seek(seconds = 0) ⇒ Object

Seeks to a given timestamp in the current track

Parameters:

  • seconds (Fixnum) (defaults to: 0)


93
94
95
96
97
# File 'lib/sonos/endpoint/a_v_transport.rb', line 93

def seek(seconds = 0)
  # Must be sent in the format of HH:MM:SS
  timestamp = Time.at(seconds).utc.strftime('%H:%M:%S')
  parse_response send_transport_message('Seek', "<Unit>REL_TIME</Unit><Target>#{timestamp}</Target>")
end

#select_track(index) ⇒ Object

Seeks the playlist selection to the provided index



100
101
102
# File 'lib/sonos/endpoint/a_v_transport.rb', line 100

def select_track(index)
  parse_response send_transport_message('Seek', "<Unit>TRACK_NR</Unit><Target>#{index}</Target>")
end

#set_sleep_timer(duration) ⇒ Object

Set a sleep timer up to 23:59:59 E.g. ‘00:11:00’ for 11 minutes.

Parameters:

  • duration (String)

    Duration of timer or nil to clear.



238
239
240
241
242
243
244
245
246
# File 'lib/sonos/endpoint/a_v_transport.rb', line 238

def set_sleep_timer(duration)
  if duration.nil?
    duration = ''
  elsif duration.gsub(':', '').to_i > 235959
    duration = '23:59:59'
  end

  parse_response send_transport_message('ConfigureSleepTimer', "<NewSleepTimerDuration>#{duration}</NewSleepTimerDuration>")
end

#stopObject

Stop playing.



73
74
75
# File 'lib/sonos/endpoint/a_v_transport.rb', line 73

def stop
  parse_response send_transport_message('Stop')
end

#ungroupObject

Ungroup from its current group. Trying to call this on a stereo pair slave will fail.



231
232
233
# File 'lib/sonos/endpoint/a_v_transport.rb', line 231

def ungroup
  parse_response send_transport_message('BecomeCoordinatorOfStandaloneGroup')
end