module Sonos::Endpoint::AVTransport

Constants

TRANSPORT_ENDPOINT
TRANSPORT_XMLNS

Public Instance Methods

add_rdio_to_queue(opts = {}, position = 0) click to toggle source

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)

# 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_metadata = "<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, didl_metadata, position)
end
add_spotify_to_queue(opts = {}, position = 0) click to toggle source

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)

# 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_metadata = "<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, didl_metadata, position)
 end
add_to_queue(uri, didl = '', position = 0) click to toggle source

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

# 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_queue() click to toggle source

Clear the queue

# File lib/sonos/endpoint/a_v_transport.rb, line 105
def clear_queue
  parse_response send_transport_message('RemoveAllTracksFromQueue')
end
get_player_state() click to toggle source

Get information about the state the player is in.

# 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) click to toggle source

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

# File lib/sonos/endpoint/a_v_transport.rb, line 225
def group(slave)
  slave.join(self)
end
has_music?() click to toggle source
# File lib/sonos/endpoint/a_v_transport.rb, line 35
def has_music?
  !now_playing.nil?
end
is_playing?() click to toggle source

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

# 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) click to toggle source

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

# 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) click to toggle source
# 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
next() click to toggle source

Play the next track.

# File lib/sonos/endpoint/a_v_transport.rb, line 78
def next
  parse_response send_transport_message('Next')
end
now_playing() click to toggle source

Get information about the currently playing track. @return [Hash] information about the current track.

# 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
pause() click to toggle source

Pause the currently playing track.

# File lib/sonos/endpoint/a_v_transport.rb, line 58
def pause
  parse_response send_transport_message('Pause')
end
play(uri = nil) click to toggle source

Play the currently selected track or play a stream. @param [String] uri Optional uri of the track to play. Leaving this blank, plays the current track.

# 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
previous() click to toggle source

Play the previous track.

# File lib/sonos/endpoint/a_v_transport.rb, line 83
def previous
  parse_response send_transport_message('Previous')
end
remove_from_queue(object_id) click to toggle source

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

# 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) click to toggle source

Save queue

# 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) click to toggle source

Seeks to a given timestamp in the current track @param [Fixnum] seconds

# 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) click to toggle source

Seeks the playlist selection to the provided index

# 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) click to toggle source

Set a sleep timer up to 23:59:59 E.g. ‘00:11:00’ for 11 minutes. @param duration [String] Duration of timer or nil to clear.

# 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
stop() click to toggle source

Stop playing.

# File lib/sonos/endpoint/a_v_transport.rb, line 73
def stop
  parse_response send_transport_message('Stop')
end
ungroup() click to toggle source

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

# File lib/sonos/endpoint/a_v_transport.rb, line 231
def ungroup
  parse_response send_transport_message('BecomeCoordinatorOfStandaloneGroup')
end

Private Instance Methods

send_transport_message(name, part = '<Speed>1</Speed>') click to toggle source
# File lib/sonos/endpoint/a_v_transport.rb, line 259
def send_transport_message(name, part = '<Speed>1</Speed>')
  action = "#{TRANSPORT_XMLNS}##{name}"
  message = %Q{<u:#{name} xmlns:u="#{TRANSPORT_XMLNS}"><InstanceID>0</InstanceID>#{part}</u:#{name}>}
  transport_client.call(name, soap_action: action, message: message)
end
set_av_transport_uri(uri) click to toggle source

Play a stream.

# File lib/sonos/endpoint/a_v_transport.rb, line 251
def set_av_transport_uri(uri)
  send_transport_message('SetAVTransportURI', "<CurrentURI>#{uri}</CurrentURI><CurrentURIMetaData></CurrentURIMetaData>")
end
transport_client() click to toggle source
# File lib/sonos/endpoint/a_v_transport.rb, line 255
def transport_client
  @transport_client ||= Savon.client endpoint: "http://#{self.group_master.ip}:#{Sonos::PORT}#{TRANSPORT_ENDPOINT}", namespace: Sonos::NAMESPACE, log: Sonos.logging_enabled
end