sonos 0.3.5 → 0.3.6

Sign up to get free protection for your applications and to get access to all the features.
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