sonos 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2500064f1c4f07a8064c691be658195d7245c15f
4
- data.tar.gz: 30d941094b116b5c4998c8f6f2cbdf8862de11b9
3
+ metadata.gz: 0b7916243ad3a1b97d2b2480654b8a9413fdb36f
4
+ data.tar.gz: 327c59e209b9bb9193f799c7aded77d7f15fc697
5
5
  SHA512:
6
- metadata.gz: ae738d359b88707fbb0913d26f8760690ba497d3c7d1d9bc8cc4675e637e4e6c9f9617e2906c1aef23b407f710c15761746372d299445c8209f34116cbcb1df7
7
- data.tar.gz: 438b11f8ffa8033572c44e99bfe91540a3e634558030a248540ec130f1e73b606dc53025fd641371de1e4a8aad151b943b071f09cdf2a4d07c4b1e167e862693
6
+ metadata.gz: afbc45559d030536fd2a4c9e465ce1402a3f6dbae3a77f1893806b9e419e6589fbfe4f3b790b745ee6ecd0e76e150956fe83d414f748fead7f9f9f0eacb0b215
7
+ data.tar.gz: 7d81b054264e7bd0884b423c2cc2c034e02c0d94a53c055cdcc8a41be317656f0ae8168466173ad3d3ed70574eb79bf6375e3480579649c28be7bc2080beb269
@@ -1,4 +1,43 @@
1
- ### Version 0.3.0February 2, 2012
1
+ ### Version 0.3.6August 20, 2014
2
+
3
+ * Unbreak clearing the queue
4
+ * Add voiceover
5
+ * Improve playlist handling
6
+ * Add support to queue Rdio tracks/albums
7
+ * Add setting a sleep timer
8
+ * Allow to queue items at arbitrary positions in the playlist
9
+ * Make party mode initialization more robust
10
+ * Add support to create stereo pair
11
+
12
+ ### Version 0.3.5 — February 4, 2014
13
+
14
+ * Allow to queue Spotify tracks/albums/playlists/top lists/starred
15
+ * Add basic line in support
16
+ * Allow toggling status light
17
+
18
+ ### Version 0.3.4 — November 1, 2013
19
+
20
+ * Rework UPNP subscription / unsubscriptino process
21
+ * Add shuffle, repeat and crossfade
22
+ * Add alarms
23
+ * Add support for ZP120, Play:1 devices
24
+
25
+ ### Version 0.3.3 — June 29, 2013
26
+
27
+ * Add party mode to CLI
28
+ * Add support for Sub and Soundbar devices
29
+
30
+ ### Version 0.3.1 — June 28, 2013
31
+
32
+ * Allow to specify a non-nil default IP
33
+ * Add CLI command to list groups
34
+ * Add ability to detect if speaker has music
35
+ * Add more information to `now_playing` output
36
+ * Support add/remove to queue
37
+ * Add support for ZP80 devices
38
+ * Add party mode
39
+
40
+ ### Version 0.3.0 — February 2, 2013
2
41
 
3
42
  * System owns groups that reflect the topology
4
43
  * Group and ungroup speakers
@@ -59,6 +59,7 @@ speaker.add_to_queue 'http://assets.samsoff.es/music/Airports.mp3'
59
59
  speaker.remove_from_queue(speaker.queue[:items].last[:queue_id])
60
60
  speaker.save_queue 'Jams'
61
61
  speaker.clear_queue
62
+ speaker.set_sleep_timer '00:13:00'
62
63
  ```
63
64
 
64
65
  Or go into what the official control from Sonos, Inc. calls "Party
@@ -75,6 +76,38 @@ system.party_over
75
76
 
76
77
  All of this is based off of the raw `Sonos.system.topology`.
77
78
 
79
+ ### Services
80
+
81
+ Currently there is support to queue items from the following services, provided
82
+ the service accounts are set up:
83
+
84
+ - Spotify
85
+ - tracks
86
+ - albums
87
+ - playlists
88
+ - top lists
89
+ - starred
90
+ - Rdio
91
+ - tracks
92
+ - albums
93
+
94
+ The way to add items differs per service at moment:
95
+
96
+ For Spotify only the 'Spotify URI' is required:
97
+
98
+ ``` ruby
99
+ speaker.add_spotify_to_queue('2CwulIyrmEYwbUWzcEVIhR')
100
+ ```
101
+
102
+ Whereas for Rdio more information needs to be provided:
103
+
104
+ ``` ruby
105
+ speaker.add_rdio_to_queue({
106
+ :track => '42083055',
107
+ :album => '3944937',
108
+ :username => 'RDIO_USERNAME_HERE' })
109
+ ```
110
+
78
111
  ### CLI
79
112
 
80
113
  There is a very limited CLI right now. You can run `sonos devices` to get the IP of all of your devices.
@@ -91,6 +124,7 @@ You can also run `sonos pause_all` to pause all your Sonos groups.
91
124
  * Detect stereo pair
92
125
  * CLI client for everything
93
126
  * Nonblocking calls with Celluloid::IO
127
+ * Unified method of adding items from music services
94
128
 
95
129
  ### Features
96
130
 
@@ -98,14 +132,10 @@ You can also run `sonos pause_all` to pause all your Sonos groups.
98
132
  * Pause all (there is no play all in the controller, we could loop through and do it though)
99
133
  * Party Mode
100
134
  * Line-in
101
- * Toggle cross fade
102
- * Toggle shuffle
103
- * Set repeat mode
104
135
  * Search music library
105
136
  * Browse music library
106
137
  * Skip to song in queue
107
138
  * Alarm clock
108
- * Sleep timer
109
139
  * Pandora doesn't use the Queue. I bet things are all jacked up.
110
140
  * CONNECT (and possibly PLAY:5) line in settings
111
141
  * Source name
@@ -118,7 +148,6 @@ You can also run `sonos pause_all` to pause all your Sonos groups.
118
148
  If we are implementing everything the official Sonos Controller does, here's some more stuff:
119
149
 
120
150
  * Set zone name and icon
121
- * Create stero pair
122
151
  * Support for SUB
123
152
  * Support for DOCK
124
153
  * Support for CONNECT:AMP (not sure if this is any different from CONNECT)
@@ -3,6 +3,7 @@ require 'sonos/system'
3
3
  require 'sonos/discovery'
4
4
  require 'sonos/device'
5
5
  require 'sonos/group'
6
+ require 'sonos/features'
6
7
 
7
8
  module Sonos
8
9
  PORT = 1400
@@ -1,5 +1,6 @@
1
1
  require 'savon'
2
2
  require 'sonos/endpoint'
3
+ require 'sonos/features'
3
4
 
4
5
  module Sonos::Device
5
6
 
@@ -11,6 +12,7 @@ module Sonos::Device
11
12
  include Sonos::Endpoint::ContentDirectory
12
13
  include Sonos::Endpoint::Upnp
13
14
  include Sonos::Endpoint::Alarm
15
+ include Sonos::Features::Voiceover
14
16
 
15
17
  MODELS = {
16
18
  :'S1' => 'PLAY:1', # Released Oct 2013
@@ -48,6 +48,12 @@ module Sonos::Endpoint::AVTransport
48
48
  }
49
49
  end
50
50
 
51
+ # Returns true if the player is not in a paused or stopped state
52
+ def is_playing?
53
+ state = get_player_state[:state]
54
+ !['PAUSED_PLAYBACK', 'STOPPED'].include?(state)
55
+ end
56
+
51
57
  # Pause the currently playing track.
52
58
  def pause
53
59
  parse_response send_transport_message('Pause')
@@ -77,7 +83,7 @@ module Sonos::Endpoint::AVTransport
77
83
  def previous
78
84
  parse_response send_transport_message('Previous')
79
85
  end
80
-
86
+
81
87
  def line_in(speaker)
82
88
  set_av_transport_uri('x-rincon-stream:' + speaker.uid.sub('uuid:', ''))
83
89
  end
@@ -90,9 +96,14 @@ module Sonos::Endpoint::AVTransport
90
96
  parse_response send_transport_message('Seek', "<Unit>REL_TIME</Unit><Target>#{timestamp}</Target>")
91
97
  end
92
98
 
99
+ # Seeks the playlist selection to the provided index
100
+ def select_track(index)
101
+ parse_response send_transport_message('Seek', "<Unit>TRACK_NR</Unit><Target>#{index}</Target>")
102
+ end
103
+
93
104
  # Clear the queue
94
105
  def clear_queue
95
- parse_response parse_response send_transport_message('RemoveAllTracksFromQueue')
106
+ parse_response send_transport_message('RemoveAllTracksFromQueue')
96
107
  end
97
108
 
98
109
  # Save queue
@@ -103,9 +114,10 @@ module Sonos::Endpoint::AVTransport
103
114
  # Adds a track to the queue
104
115
  # @param[String] uri Uri of track
105
116
  # @param[String] didl Stanza of DIDL-Lite metadata (generally created by #add_spotify_to_queue)
117
+ # @param[Integer] position Optional queue insertion position. Leaving this blank inserts at the end.
106
118
  # @return[Integer] Queue position of the added track
107
- def add_to_queue(uri, didl = '')
108
- response = send_transport_message('AddURIToQueue', "<EnqueuedURI>#{uri}</EnqueuedURI><EnqueuedURIMetaData>#{didl}</EnqueuedURIMetaData><DesiredFirstTrackNumberEnqueued>0</DesiredFirstTrackNumberEnqueued><EnqueueAsNext>1</EnqueueAsNext>")
119
+ def add_to_queue(uri, didl = '', position = 0)
120
+ response = send_transport_message('AddURIToQueue', "<EnqueuedURI>#{uri}</EnqueuedURI><EnqueuedURIMetaData>#{didl}</EnqueuedURIMetaData><DesiredFirstTrackNumberEnqueued>#{position}</DesiredFirstTrackNumberEnqueued><EnqueueAsNext>1</EnqueueAsNext>")
109
121
  # TODO yeah, this error handling is a bit soft. For consistency's sake :)
110
122
  pos = response.xpath('.//FirstTrackNumberEnqueued').text
111
123
  if pos.length != 0
@@ -115,8 +127,9 @@ module Sonos::Endpoint::AVTransport
115
127
 
116
128
  # Adds a Spotify track to the queue along with extra data for better metadata retrieval
117
129
  # @param[Hash] opts Various options (id, user, region and type)
130
+ # @param[Integer] position Optional queue insertion position. Leaving this blank inserts at the end.
118
131
  # @return[Integer] Queue position of the added track(s)
119
- def add_spotify_to_queue(opts = {})
132
+ def add_spotify_to_queue(opts = {}, position = 0)
120
133
  opts = {
121
134
  :id => '',
122
135
  :user => nil,
@@ -153,7 +166,46 @@ module Sonos::Endpoint::AVTransport
153
166
  return nil
154
167
  end
155
168
 
156
- add_to_queue(uri, didl_metadata)
169
+ add_to_queue(uri, didl_metadata, position)
170
+ end
171
+
172
+ # Add an Rdio object to the queue (album or track), anything else can only
173
+ # be streamed (play now).
174
+ # @param[Hash] opts Various options (album/track keys, username and type)
175
+ # @param[Integer] position Optional queue insertion position. Leaving this blank inserts at the end.
176
+ # @return[Integer] Queue position of the added track(s)
177
+ def add_rdio_to_queue(opts = {}, position = 0)
178
+ opts = {
179
+ :username => nil,
180
+ :album => nil,
181
+ :track => nil,
182
+ :type => 'track',
183
+ :format => 'mp3' # can be changed, but only 'mp3' is valid.
184
+ }.merge(opts)
185
+
186
+ return nil if opts[:username].nil?
187
+
188
+ # Both tracks and albums require the album key. And tracks need a track
189
+ # key of course.
190
+ return nil if opts[:album].nil?
191
+ return nil if opts[:type] == 'track' and opts[:track].nil?
192
+
193
+ # In order for valid DIDL we'll pass an empty :track for albums.
194
+ opts[:track] = '' if opts[:type] == 'album'
195
+
196
+ didl_metadata = "&lt;DIDL-Lite xmlns:dc=&quot;http://purl.org/dc/elements/1.1/&quot; xmlns:upnp=&quot;urn:schemas-upnp-org:metadata-1-0/upnp/&quot; xmlns:r=&quot;urn:schemas-rinconnetworks-com:metadata-1-0/&quot; xmlns=&quot;urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/&quot;&gt;&lt;item id=&quot;00030020_t%3a%3a#{opts[:track]}%3a%3aa%3a%3a#{opts[:album]}&quot; parentID=&quot;0004006c_a%3a%3a#{opts[:album]}&quot; restricted=&quot;true&quot;&gt;&lt;dc:title&gt;&lt;/dc:title&gt;&lt;upnp:class&gt;object.item.audioItem.musicTrack&lt;/upnp:class&gt;&lt;desc id=&quot;cdudn&quot; nameSpace=&quot;urn:schemas-rinconnetworks-com:metadata-1-0/&quot;&gt;SA_RINCON2823_#{opts[:username]}&lt;/desc&gt;&lt;/item&gt;&lt;/DIDL-Lite&gt;"
197
+
198
+ case opts[:type]
199
+ when /track/
200
+ uri = "x-sonos-http:_t%3a%3a#{opts[:track]}%3a%3aa%3a%3a#{opts[:album]}.#{opts[:format]}?sid=11&amp;flags=32"
201
+ when /album/
202
+ type_id = '0004006c_a'
203
+ uri = "x-rincon-cpcontainer:#{type_id}%3a%3a#{opts[:album]}"
204
+ else
205
+ return nil
206
+ end
207
+
208
+ add_to_queue(uri, didl_metadata, position)
157
209
  end
158
210
 
159
211
  # Removes a track from the queue
@@ -180,6 +232,19 @@ module Sonos::Endpoint::AVTransport
180
232
  parse_response send_transport_message('BecomeCoordinatorOfStandaloneGroup')
181
233
  end
182
234
 
235
+ # Set a sleep timer up to 23:59:59
236
+ # E.g. '00:11:00' for 11 minutes.
237
+ # @param duration [String] Duration of timer or nil to clear.
238
+ def set_sleep_timer(duration)
239
+ if duration.nil?
240
+ duration = ''
241
+ elsif duration.gsub(':', '').to_i > 235959
242
+ duration = '23:59:59'
243
+ end
244
+
245
+ parse_response send_transport_message('ConfigureSleepTimer', "<NewSleepTimerDuration>#{duration}</NewSleepTimerDuration>")
246
+ end
247
+
183
248
  private
184
249
 
185
250
  # Play a stream.
@@ -16,6 +16,22 @@ module Sonos::Endpoint::Device
16
16
  parse_response send_device_message('SetLEDState', enabled ? 'On' : 'Off')
17
17
  end
18
18
 
19
+ # Create a stereo pair of two speakers.
20
+ # This does not take into account which type of players support bonding.
21
+ # Currently only S1/S3 (play:1/play:3) support this but future players may
22
+ # gain this abbility too. The speaker on which this method is called is
23
+ # assumed to be the left speaker of the pair.
24
+ # @param right [Sonos::Device::Speaker] Right speaker
25
+ def create_pair_with(right)
26
+ left = self.uid.sub!('uuid:', '')
27
+ right = right.uid.sub!('uuid:', '')
28
+ parse_response = send_bonding_message('AddBondedZones', "#{left}:LF,LF;#{right}:RF,RF")
29
+ end
30
+
31
+ def separate_pair
32
+ parse_response = send_bonding_message('RemoveBondedZones', '')
33
+ end
34
+
19
35
  private
20
36
 
21
37
  def device_client
@@ -28,4 +44,10 @@ private
28
44
  message = %Q{<u:#{name} xmlns:u="#{DEVICE_XMLNS}"><Desired#{attribute}>#{value}</Desired#{attribute}>}
29
45
  device_client.call(name, soap_action: action, message: message)
30
46
  end
47
+
48
+ def send_bonding_message(name, value)
49
+ action = "#{DEVICE_XMLNS}##{name}"
50
+ message = %Q{<u:#{name} xmlns:u="#{DEVICE_XMLNS}"><ChannelMapSet>#{value}</ChannelMapSet></u:#{name}>}
51
+ device_client.call(name, soap_action: action, message: message)
52
+ end
31
53
  end
@@ -0,0 +1,6 @@
1
+ module Sonos
2
+ module Features
3
+ end
4
+ end
5
+
6
+ require 'sonos/features/voiceover'
@@ -0,0 +1,56 @@
1
+ module Sonos::Features::Voiceover
2
+
3
+ # Interrupts the speaker and plays the provided URI. When finished, returns the play head
4
+ # and state to their original position. Useful for doorbell sounds, announcements, etc.
5
+ def voiceover!(uri, vol = nil)
6
+ start_time = Time.now
7
+
8
+ result = group_master.with_isolated_state do
9
+ self.volume = vol if vol
10
+ group_master.play_blocking(uri)
11
+ end
12
+
13
+ result.merge({duration: (Time.now - start_time )})
14
+ end
15
+
16
+ protected
17
+
18
+ def with_isolated_state
19
+ pause if was_playing = is_playing?
20
+ unmute if was_muted = muted?
21
+ previous_volume = volume
22
+ previous = now_playing
23
+
24
+ yield
25
+
26
+ # the sonos app does this. I think it tells the player to think of the master queue as active again
27
+ play uid.gsub('uuid', 'x-rincon-queue') + '#0'
28
+
29
+ if previous
30
+ select_track previous[:queue_position]
31
+ seek Time.parse("1/1/1970 #{previous[:current_position]} -0000" ).to_i
32
+
33
+ self.volume = previous_volume
34
+ mute if was_muted
35
+ end
36
+
37
+ play if was_playing
38
+
39
+ {
40
+ original_volume: previous_volume,
41
+ original_state: (was_playing ? 'playing' : 'paused')
42
+ }
43
+ end
44
+
45
+ def play_blocking(uri)
46
+ # queue up the track
47
+ play uri
48
+
49
+ # play it
50
+ play
51
+
52
+ # pause the thread until the track is done
53
+ sleep(0.1) while is_playing?
54
+ end
55
+
56
+ end
@@ -47,9 +47,12 @@ module Sonos
47
47
 
48
48
  def find_party_master
49
49
  # 1: If there are any pre-existing groups playing something, use
50
- # the lowest-numbered group's master
50
+ # the lowest-numbered group's master. But ensure to only check a
51
+ # master_speaker that is actually a speaker, and not an Accessory.
51
52
  groups.each do |group|
52
- return group.master_speaker if group.master_speaker.has_music?
53
+ if group.master_speaker.speaker? and group.master_speaker.has_music?
54
+ return group.master_speaker
55
+ end
53
56
  end
54
57
 
55
58
  # 2: Lowest-number speaker that's playing something
@@ -60,18 +63,18 @@ module Sonos
60
63
  # 3: lowest-numbered speaker
61
64
  speakers[0]
62
65
  end
63
-
66
+
64
67
  # Party's over :(
65
68
  def party_over
66
69
  groups.each { |g| g.disband }
67
70
  rescan @topology
68
71
  end
69
-
72
+
70
73
  def rescan(topology = Discovery.new.topology)
71
74
  @topology = topology
72
75
  @groups = []
73
76
  @devices = @topology.collect(&:device)
74
-
77
+
75
78
  construct_groups
76
79
 
77
80
  speakers.each do |speaker|
@@ -102,6 +105,9 @@ module Sonos
102
105
  master = node if node.coordinator == "true"
103
106
  end
104
107
 
108
+ # Skip this group if there is no master
109
+ next if master.nil?
110
+
105
111
  # register other nodes in groups as slave nodes
106
112
  nodes = []
107
113
  @topology.each do |node|
@@ -110,9 +116,6 @@ module Sonos
110
116
  nodes << node unless node.uuid == master.uuid
111
117
  end
112
118
 
113
- # Skip this group if there is no master
114
- next if master.nil?
115
-
116
119
  # Add the group
117
120
  @groups << Group.new(master.device, nodes.collect(&:device))
118
121
  end
@@ -1,3 +1,3 @@
1
1
  module Sonos
2
- VERSION = '0.3.5'
2
+ VERSION = '0.3.6'
3
3
  end
@@ -6,8 +6,8 @@ require 'sonos/version'
6
6
  Gem::Specification.new do |gem|
7
7
  gem.name = 'sonos'
8
8
  gem.version = Sonos::VERSION
9
- gem.authors = ['Sam Soffes', 'Aaron Gotwalt']
10
- gem.email = ['sam@soff.es', 'gotwalt@gmail.com']
9
+ gem.authors = ['Sam Soffes', 'Aaron Gotwalt', 'Jasper Lievisse Adriaanse']
10
+ gem.email = ['sam@soff.es', 'gotwalt@gmail.com', 'jasper@humppa.nl']
11
11
  gem.description = 'Control Sonos speakers with Ruby'
12
12
  gem.summary = gem.description
13
13
  gem.homepage = 'https://github.com/soffes/sonos'
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sonos
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Soffes
8
8
  - Aaron Gotwalt
9
+ - Jasper Lievisse Adriaanse
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2014-02-04 00:00:00.000000000 Z
13
+ date: 2014-08-20 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: savon
@@ -71,6 +72,7 @@ description: Control Sonos speakers with Ruby
71
72
  email:
72
73
  - sam@soff.es
73
74
  - gotwalt@gmail.com
75
+ - jasper@humppa.nl
74
76
  executables:
75
77
  - sonos
76
78
  extensions: []
@@ -98,6 +100,8 @@ files:
98
100
  - lib/sonos/endpoint/device.rb
99
101
  - lib/sonos/endpoint/rendering.rb
100
102
  - lib/sonos/endpoint/upnp.rb
103
+ - lib/sonos/features.rb
104
+ - lib/sonos/features/voiceover.rb
101
105
  - lib/sonos/group.rb
102
106
  - lib/sonos/system.rb
103
107
  - lib/sonos/topology_node.rb