ruby-mpd 0.1.5 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -26,7 +26,7 @@ Then, make a new MPD instance:
26
26
 
27
27
  mpd = MPD.new 'localhost', 6600
28
28
 
29
- You can also omit the host and/or port, and it will use the defaults.
29
+ You can also omit the host and port, and it will use the defaults.
30
30
 
31
31
  mpd = MPD.new 'localhost'
32
32
  mpd = MPD.new
@@ -43,7 +43,7 @@ When you are done, disconnect by calling disconnect.
43
43
  be fixed by enabling callbacks (see the Callbacks section) or by issuing a
44
44
  `ping` command at certain intervals
45
45
 
46
- Once connected, you can issue commands to talk to the server
46
+ Once connected, you can issue commands to talk to the server.
47
47
 
48
48
  mpd.connect
49
49
 
@@ -52,34 +52,45 @@ Once connected, you can issue commands to talk to the server
52
52
  song = mpd.current_song
53
53
  puts "Current Song: #{song.artist} - #{song.title}"
54
54
 
55
- You can find documentation for each command at http://www.rubydoc.info/github/archSeer/ruby-mpd/master/MPD
55
+ You can find command documentation can be found {here}[http://www.rubydoc.info/github/archSeer/ruby-mpd/master/MPD].
56
56
 
57
57
  == Commands
58
58
 
59
+ Some commands require URI paths. ruby-mpd allows you to use MPD::Song objects directly
60
+ and it extracts the file paths behind the scenes.
61
+
62
+ song = mpd.songs_by_artist('Elvis Presley').first # => MPD::Song
63
+ mpd.add song
64
+
65
+ === Options
66
+
59
67
  Some commands accept "option hashes" besides their default values. For example, +#move+
60
68
  accepts an ID key instead of the position:
61
69
 
62
70
  mpd.move(1, 10) # => move first song to position 10.
63
-
64
- mpd.move(:id => 1, 10) # => move the song with the ID of 1 to position 10.
71
+ mpd.move({:id => 1}, 10) # => move the song with the ID of 1 to position 10.
65
72
 
66
73
  Commands that accept ID's: +#move+, +#delete+, +#play+, +#song_priority+. +#seek+
67
- accepts both +:pos+ and +:id+. *Note*: #swap and #swapid are still separate!
74
+ accepts both +:pos+ and +:id+. *Note*: +#swap+ and +#swapid+ are still separate!
68
75
 
69
76
  === Ranges
70
77
 
71
78
  Some commands also allow ranges instead of numbers, specifying a range of songs.
72
- ruby-mpd correctly handles inclusive and exclusive ranges (1..10 vs 1...10).
73
- For example, #queue allows us to return only a subset of the queue:
79
+ ruby-mpd correctly handles inclusive and exclusive ranges (1..10 vs 1...10). Negative
80
+ range end means that we want the range to span until the end of the list.
81
+
82
+ For example, +#queue+ allows us to return only a subset of the queue:
74
83
 
75
84
  mpd.queue.count # => 20
76
85
 
77
86
  mpd.queue(1..10).count # => 10
78
87
 
79
- Move also allows this:
88
+ mpd.queue(5..-1).count # => 15 (from 5 to the end of the range)
89
+ mpd.queue(5...-1).count # => 15 (does the same)
80
90
 
81
- mpd.move 1, 10 # => move song 1 to position 10.
91
+ Move also allows specifying ranges to move a range of songs instead of just one.
82
92
 
93
+ mpd.move 1, 10 # => move song 1 to position 10.
83
94
  mpd.move 1..3, 10 # => move songs 1, 2 and 3 to position 10 (and 11 and 12).
84
95
 
85
96
  Commands that support ranges: +#delete+, +#move+, +#queue+, +#song_priority+, +#shuffle+,
@@ -88,7 +99,7 @@ Commands that support ranges: +#delete+, +#move+, +#queue+, +#song_priority+, +#
88
99
  == Searching
89
100
 
90
101
  Searching is case insensitive by default. To enable case sensitivity, pass a third
91
- argument in as +:case_sensitive => true+. This does not work for Playlist#searchadd.
102
+ argument with the key +:case_sensitive+. This does not work for Playlist#searchadd.
92
103
 
93
104
  mpd.search(:artist, 'MyArtiSt', :case_sensitive => true)
94
105
 
@@ -104,7 +115,7 @@ Playlists are one of the objects that map the MPD commands onto a simple to use
104
115
  object. Instead of going trough all those function calls, passing data along to
105
116
  get your results, you simply use the object in an object-oriented way:
106
117
 
107
- mpd.playlists # 0> [MPD::Playlist, MPD::Playlist...]
118
+ mpd.playlists # => [MPD::Playlist, MPD::Playlist...]
108
119
 
109
120
  playlist = mpd.playlists.first
110
121
 
@@ -124,8 +135,8 @@ action on them (add, delete, rename).
124
135
 
125
136
  MPD::Playlist.new(mpd, 'name')
126
137
 
127
- Currently, one also has to pass in the MPD connection, as playlists are tied to
128
- a certain connection.
138
+ Currently, one also has to pass in the MPD instance, as playlists are tied to a
139
+ certain connection.
129
140
 
130
141
  == Callbacks
131
142
 
@@ -139,24 +150,24 @@ the server without having to worry about timeouts.
139
150
  To make use of callbacks, we need to:
140
151
 
141
152
  1. Setup a callback to be called when something happens.
142
- 2. Connect to the server with callbacks set as enabled.
153
+ 2. Connect to the server with callbacks enabled.
143
154
 
144
155
  Firstly, we need to create a callback block and subscribe it, so that will get
145
156
  triggered whenever a specific event happens. When the callback is triggered,
146
157
  it will also recieve the new values of the event that happened.
147
158
 
148
- So how do we do this? We use the MPD#on method, which sets it all up for us. The
159
+ So how do we do this? We use the +MPD#on+ method, which sets it all up for us. The
149
160
  argument takes a symbol with the name of the event. The function also requires a block,
150
161
  which is our actual callback that will get called.
151
162
 
152
163
  mpd.on :volume do |volume|
153
- puts "Volume was set to #{volume}"!
164
+ puts "Volume was set to #{volume}!"
154
165
  end
155
166
 
156
167
  One can also use separate methods or Procs and whatnot, just pass them in as a parameter.
157
168
 
158
169
  # Proc
159
- proc = Proc.new {|volume| puts "Volume was set to #{volume}"! }
170
+ proc = Proc.new {|volume| puts "Volume was set to #{volume}!" }
160
171
  mpd.on :volume, &proc
161
172
 
162
173
  # Method
@@ -167,32 +178,33 @@ One can also use separate methods or Procs and whatnot, just pass them in as a p
167
178
  method = self.method(:volume_change)
168
179
  mpd.on :volume, &method
169
180
 
170
- ruby-mpd supports callbacks for any of the keys returned by MPD#status, as well as +:connection+.
171
- which will notify us when we connect or disconnect to the daemon. Here's the full list of events,
172
- along with the variables it will return:
173
-
174
- * *volume*: The volume level as an Integer between 0-100.
175
- * *repeat*: true or false
176
- * *random*: true or false
177
- * *single*: true or false
178
- * *consume*: true or false
179
- * *playlist*: 31-bit unsigned Integer, the playlist version number.
180
- * *playlistlength*: Integer, the length of the playlist
181
- * *state*: :play, :stop, or :pause, state of the playback.
182
- * *song*: An MPD::Song object, representing the current song.
183
- * *songid*: playlist songid of the current song stopped on or playing.
184
- * *nextsong*: playlist song number of the next song to be played.
185
- * *nextsongid*: playlist songid of the next song to be played.
186
- * *time*: Returns two variables, *+total+* and *+elapsed+*, Integers representing seconds.
187
- * *elapsed*: Float, representing total time elapsed within the current song, but with higher accuracy.
188
- * *bitrate*: instantaneous bitrate in kbps.
189
- * *xfade*: crossfade in seconds
190
- * *mixrampdb*: mixramp threshold in dB (Float)
191
- * *mixrampdelay*: mixrampdelay in seconds
192
- * *audio*: Returns three variables: sampleRate, bits and channels.
193
- * *updating_db*: job id
194
- * *error*: if there is an error, returns message here
195
-
181
+ ruby-mpd supports callbacks for any of the keys returned by +MPD#status+, as well as +:connection+.
182
+ Here's the full list of events, along with the variables it will return:
183
+
184
+ * *volume*: The volume level as an Integer between 0-100.
185
+ * *repeat*: true or false
186
+ * *random*: true or false
187
+ * *single*: true or false
188
+ * *consume*: true or false
189
+ * *playlist*: 31-bit unsigned Integer, the playlist version number.
190
+ * *playlistlength*: Integer, the length of the playlist
191
+ * *state*: :play, :stop, or :pause, state of the playback.
192
+ * *song*: An MPD::Song object, representing the current song.
193
+ * *songid*: playlist songid of the current song stopped on or playing.
194
+ * *nextsong*: playlist song number of the next song to be played.
195
+ * *nextsongid*: playlist songid of the next song to be played.
196
+ * *time*: Returns two variables, *+total+* and *+elapsed+*, Integers representing seconds.
197
+ * *elapsed*: Float, representing total time elapsed within the current song, but with higher accuracy.
198
+ * *bitrate*: instantaneous bitrate in kbps.
199
+ * *xfade*: crossfade in seconds
200
+ * *mixrampdb*: mixramp threshold in dB (Float)
201
+ * *mixrampdelay*: mixrampdelay in seconds
202
+ * *audio*: Returns three variables: sampleRate, bits and channels.
203
+ * *updating_db*: job id
204
+ * *error*: if there is an error, returns message here
205
+
206
+ * *connection*: Are we connected to the daemon? true or false
207
+
196
208
  Note that if the callback returns more than one value, the callback needs more arguments
197
209
  in order to recieve those values:
198
210
 
@@ -208,10 +220,53 @@ in order to recieve those values:
208
220
  Finally, the easiest step. In order for callbacks to work, connect to the server
209
221
  with callbacks enabled:
210
222
 
211
- mpd.connect true
223
+ mpd.connect true
212
224
 
213
225
  Easy as pie. The above will connect to the server like normal, but this time it will
214
226
  create a new thread that loops until you issue a `disconnect`. This loop checks the
215
227
  server, then sleeps for two tenths of a second, then loops. Because it's continuously
216
228
  polling the server, there's the added benefit of your client not being disconnected
217
229
  due to inactivity.
230
+
231
+ == Not yet implemented
232
+
233
+ This section documents the features that are missing in this library at the moment.
234
+
235
+ === Command lists
236
+
237
+ Command lists are not implemented yet. The proposed API would look like:
238
+
239
+ mpd.command_list do
240
+ volume 80
241
+ repeat true
242
+ status
243
+ end
244
+
245
+ What makes me not so eager to implement this is that MPD returns all values one after
246
+ another. This gets fixed with +command_list_ok_begin+, which returns +list_OK+ for every
247
+ command used, however then we still get more than one response, and I can't think of a
248
+ reasonable way to retun all of them back to the user. Maybe just ignore the return values?
249
+
250
+ === Idle
251
+
252
+ To implement idle, what is needed is a lock that prevents sending commands to the daemon
253
+ while waiting for the response (except +noidle+). An intermediate solution would be to
254
+ queue the commands to send them later, when idle has returned the response.
255
+
256
+ Idle seems like a possible way to reimplement callbacks; make a separate connection
257
+ and just use idle and when it returns, simply use idle again and again.
258
+
259
+ === Tests
260
+
261
+ Tests fail at the moment, as they are 6 years old. The entire MPD server mock class
262
+ either needs to be rewritten, or a mpd.conf along with a sample database and instructions
263
+ for a controlled environment needs to be written.
264
+
265
+ == TODO list
266
+
267
+ * MPD::Song, MPD::Directory.
268
+ * Make stickers a mixin for Playlist, Song, Directory...
269
+ * Namespace queue
270
+ * Make parsing per-command (because playlist is an integer in the :status command and
271
+ a string in :listplaylists, and :time is an array in status and integer in songs)
272
+ * Per command parsing should also skip :directory and :playlist keys inside #songs command.
data/lib/ruby-mpd.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'socket'
2
2
  require 'thread'
3
3
 
4
+ require 'ruby-mpd/exceptions'
4
5
  require 'ruby-mpd/song'
5
6
  require 'ruby-mpd/parser'
6
7
  require 'ruby-mpd/playlist'
@@ -16,27 +17,6 @@ require 'ruby-mpd/plugins/outputs'
16
17
  require 'ruby-mpd/plugins/reflection'
17
18
  require 'ruby-mpd/plugins/channels'
18
19
 
19
- # TODO: object oriented: song commands and dir commands
20
- # in MPD::Song, MPD::Directory.
21
- # Make stickers a mixin for Playlist, Song, Directory...
22
- # TODO: Namespace queue?
23
- # TODO: fix parser to use build_group also
24
-
25
- # command list as a do block
26
- # mpd.command_list do
27
- # volume 10
28
- # play xyz
29
- # end
30
-
31
- # Playlist#rename -> Playlist#name= ?
32
- # MPD::Playlist.new(mpd, 'name') no mpd?
33
-
34
- # make it possible to use MPD::Song objects instead of filepath strings
35
-
36
- # commands missing: #plchangeposid, #lsinfo, #listall.
37
-
38
- # error codes stored in ack.h
39
-
40
20
  # @!macro [new] error_raise
41
21
  # @raise (see #send_command)
42
22
  # @!macro [new] returnraise
@@ -45,10 +25,6 @@ require 'ruby-mpd/plugins/channels'
45
25
 
46
26
  # The main class/namespace of the MPD client.
47
27
  class MPD
48
-
49
- # Standard MPD error.
50
- class MPDError < StandardError; end
51
-
52
28
  include Parser
53
29
 
54
30
  include Plugins::Information
@@ -64,8 +40,6 @@ class MPD
64
40
 
65
41
  # The version of the MPD protocol the server is using.
66
42
  attr_reader :version
67
- # A list of tags MPD accepts.
68
- attr_reader :tags
69
43
 
70
44
  # Initialize an MPD object with the specified hostname and port.
71
45
  # When called without arguments, 'localhost' and 6600 are used.
@@ -74,6 +48,7 @@ class MPD
74
48
  @port = port
75
49
  @socket = nil
76
50
  @version = nil
51
+ @tags = nil
77
52
 
78
53
  @stop_cb_thread = false
79
54
  @mutex = Mutex.new
@@ -85,15 +60,18 @@ class MPD
85
60
  # that specific event happens.
86
61
  #
87
62
  # mpd.on :volume do |volume|
88
- # puts "Volume was set to #{volume}"!
63
+ # puts "Volume was set to #{volume}!"
89
64
  # end
90
65
  #
91
66
  # One can also define separate methods or Procs and whatnot,
92
67
  # just pass them in as a parameter.
93
68
  #
94
- # method = Proc.new {|volume| puts "Volume was set to #{volume}"! }
69
+ # method = Proc.new {|volume| puts "Volume was set to #{volume}!" }
95
70
  # mpd.on :volume, &method
96
71
  #
72
+ # @param [Symbol] event The event we wish to listen for.
73
+ # @param [Proc, Method] block The actual callback.
74
+ # @return [void]
97
75
  def on(event, &block)
98
76
  @callbacks[event] ||= []
99
77
  @callbacks[event].push block
@@ -101,6 +79,7 @@ class MPD
101
79
 
102
80
  # Triggers an event, running it's callbacks.
103
81
  # @param [Symbol] event The event that happened.
82
+ # @return [void]
104
83
  def emit(event, *args)
105
84
  @callbacks[event] ||= []
106
85
  @callbacks[event].each do |handle|
@@ -120,7 +99,7 @@ class MPD
120
99
  # @return [true] Successfully connected.
121
100
  # @raise [MPDError] If connect is called on an already connected instance.
122
101
  def connect(callbacks = false)
123
- raise MPDError, 'Already Connected!' if self.connected?
102
+ raise ConnectionError, 'Already connected!' if self.connected?
124
103
 
125
104
  @socket = File.exists?(@hostname) ? UNIXSocket.new(@hostname) : TCPSocket.new(@hostname, @port)
126
105
  @version = @socket.gets.chomp.gsub('OK MPD ', '') # Read the version
@@ -182,14 +161,16 @@ class MPD
182
161
  # Disconnect from the MPD daemon. This has no effect if the client is not
183
162
  # connected. Reconnect using the {#connect} method. This will also stop
184
163
  # the callback thread, thus disabling callbacks.
164
+ # @return [Boolean] True if successfully disconnected, false otherwise.
185
165
  def disconnect
186
166
  @stop_cb_thread = true
187
167
 
188
- return if @socket.nil?
168
+ return false if !@socket
189
169
 
190
170
  @socket.puts 'close'
191
171
  @socket.close
192
172
  @socket = nil
173
+ return true
193
174
  end
194
175
 
195
176
  # Kills the MPD process.
@@ -198,8 +179,9 @@ class MPD
198
179
  send_command :kill
199
180
  end
200
181
 
201
- # Used for authentication with the server
182
+ # Used for authentication with the server.
202
183
  # @param [String] pass Plaintext password
184
+ # @macro returnraise
203
185
  def password(pass)
204
186
  send_command :password, pass
205
187
  end
@@ -210,64 +192,26 @@ class MPD
210
192
  send_command :ping
211
193
  end
212
194
 
213
- ###--- OTHER ---###
214
-
215
- # Lists all of the albums in the database.
216
- # The optional argument is for specifying an artist to list
217
- # the albums for
218
- #
219
- # @return [Array<String>] An array of album names.
220
- def albums(artist = nil)
221
- list :album, artist
222
- end
223
-
224
- # Lists all of the artists in the database.
225
- #
226
- # @return [Array<String>] An array of artist names.
227
- def artists
228
- list :artist
229
- end
230
-
231
- # List all of the directories in the database, starting at path.
232
- # If path isn't specified, the root of the database is used
233
- #
234
- # @return [Array<String>] Array of directory names
235
- def directories(path = nil)
236
- response = send_command :listall, path
237
- return response[:directory]
238
- end
239
-
240
- # List all of the files in the database, starting at path.
241
- # If path isn't specified, the root of the database is used
195
+ # Used to send a command to the server, and to recieve the reply.
196
+ # Reply gets parsed. Synchronized on a mutex to be thread safe.
242
197
  #
243
- # @return [Array<String>] Array of file names
244
- def files(path = nil)
245
- response = send_command(:listall, path)
246
- return response[:file]
247
- end
248
-
249
- # List all of the songs by an artist.
250
- #
251
- # @return [Array<MPD::Song>]
252
- def songs_by_artist(artist)
253
- search :artist, artist
254
- end
255
-
256
- # Used to send a command to the server. This synchronizes
257
- # on a mutex to be thread safe
198
+ # Can be used to get low level direct access to MPD daemon. Not
199
+ # recommended, should be just left for internal use by other
200
+ # methods.
258
201
  #
259
202
  # @return (see #handle_server_response)
260
203
  # @raise [MPDError] if the command failed.
261
204
  def send_command(command, *args)
262
- raise MPDError, "Not Connected to the Server" if @socket.nil?
205
+ raise ConnectionError, "Not connected to the server!" if @socket.nil?
263
206
 
264
207
  @mutex.synchronize do
265
208
  begin
266
209
  @socket.puts convert_command(command, *args)
267
- return handle_server_response
210
+ response = handle_server_response
211
+ return parse_response(command, response)
268
212
  rescue Errno::EPIPE
269
213
  @socket = nil
270
- raise MPDError, 'Broken Pipe (Disconnected)'
214
+ raise ConnectionError, 'Broken pipe (got disconnected)'
271
215
  end
272
216
  end
273
217
  end
@@ -275,8 +219,7 @@ class MPD
275
219
  private
276
220
 
277
221
  # Handles the server's response (called inside {#send_command}).
278
- # Repeatedly reads the server's response from the socket and
279
- # processes the output.
222
+ # Repeatedly reads the server's response from the socket.
280
223
  #
281
224
  # @return (see Parser#build_response)
282
225
  # @return [true] If "OK" is returned.
@@ -302,11 +245,27 @@ class MPD
302
245
 
303
246
  if !error
304
247
  return true if msg.empty?
305
- return build_response(msg)
248
+ return msg
306
249
  else
307
250
  err = error.match(/^ACK \[(?<code>\d+)\@(?<pos>\d+)\] \{(?<command>.*)\} (?<message>.+)$/)
308
- raise MPDError, "#{err[:code]}: #{err[:command]}: #{err[:message]}"
251
+ raise SERVER_ERRORS[err[:code].to_i], "[#{err[:command]}] #{err[:message]}"
309
252
  end
310
253
  end
311
254
 
255
+ SERVER_ERRORS = {
256
+ 1 => NotListError,
257
+ 2 => ServerArgumentError,
258
+ 3 => IncorrectPassword,
259
+ 4 => PermissionError,
260
+ 5 => ServerError,
261
+
262
+ 50 => NotFound,
263
+ 51 => PlaylistMaxError,
264
+ 52 => SystemError,
265
+ 53 => PlaylistLoadError,
266
+ 54 => AlreadyUpdating,
267
+ 55 => NotPlaying,
268
+ 56 => AlreadyExists
269
+ }
270
+
312
271
  end