require 'uri' module Sonos::Endpoint::AVTransport TRANSPORT_ENDPOINT = '/MediaRenderer/AVTransport/Control' TRANSPORT_XMLNS = 'urn:schemas-upnp-org:service:AVTransport:1' # Get information about the currently playing track. # @return [Hash] information about the current track. 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 def has_music? !now_playing.nil? end # Pause the currently playing track. def pause send_transport_message('Pause') end # 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. def play(uri = nil) # Play a song from the uri set_av_transport_uri(uri) and return if uri # Play the currently selected track send_transport_message('Play') end # Stop playing. def stop send_transport_message('Stop') end # Play the next track. def next send_transport_message('Next') end # Play the previous track. def previous send_transport_message('Previous') end # Seeks to a given timestamp in the current track # @param [Fixnum] seconds def seek(seconds = 0) # Must be sent in the format of HH:MM:SS timestamp = Time.at(seconds).utc.strftime('%H:%M:%S') send_transport_message('Seek', "REL_TIME#{timestamp}") end # Clear the queue def clear_queue send_transport_message('RemoveAllTracksFromQueue') end # Save queue def save_queue(title) send_transport_message('SaveQueue', "#{title}") end # Adds a track to the queue # @param[String] uri Uri of track # @return[Integer] Queue position of the added track def add_to_queue(uri) response = send_transport_message('AddURIToQueue', "#{uri}01") # 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 # Removes a track from the queue # @param[String] object_id Track's queue ID def remove_from_queue(object_id) send_transport_message('RemoveTrackFromQueue', "#{object_id}0") end # Join another speaker's group. # Trying to call this on a stereo pair slave will fail. def join(master) set_av_transport_uri('x-rincon:' + master.uid.sub('uuid:', '')) end # Add another speaker to this group. # Trying to call this on a stereo pair slave will fail. def group(slave) slave.join(self) end # Ungroup from its current group. # Trying to call this on a stereo pair slave will fail. def ungroup send_transport_message('BecomeCoordinatorOfStandaloneGroup') end private # Play a stream. def set_av_transport_uri(uri) send_transport_message('SetAVTransportURI', "#{uri}") end def transport_client @transport_client ||= Savon.client endpoint: "http://#{self.group_master.ip}:#{Sonos::PORT}#{TRANSPORT_ENDPOINT}", namespace: Sonos::NAMESPACE, log_level: :error end def send_transport_message(name, part = '1') action = "#{TRANSPORT_XMLNS}##{name}" message = %Q{0#{part}} transport_client.call(name, soap_action: action, message: message) end end