a mpd lib. certainly better than `mpc ...` :)
[mpdhero.git] / mpd.rb
blob70ca90d68a536b2044abe15b1ac8a9f106ac05a3
1 #!/usr/bin/ruby -w
2 require 'socket'
4 #== mpd.rb
6 #mpd.rb is the Ruby MPD Library
8 #Written for MPD 0.11.5 (see http://www.musicpd.org for MPD itself)
10 #The MPD class provides an interface for communicating with an MPD server (MPD = Music Player
11 #Daemon, a 'jukebox' server that plays various audio files like mp3, Ogg Vorbis, etc -- see
12 #www.musicpd.org for more about MPD itself). Method names largely correspond to the same command
13 #with the MPD protocol itself, and other MPD tools, like mpc.  Some convenience methods for
14 #writing clients are included as well.
16 #== Usage
17
18 #The default host is 'localhost'. The default port is 6600.
19 #If the user has environment variables MPD_HOST or MPD_PORT set, these
20 #will override the default settings.
22 #mpd.rb makes no attempt to keep the socket alive. If it dies it just opens a new socket.
24 #If your MPD server requires a password, you will need to use MPD#password= or MPD#password(pass)
25 #before you can use any other server command. Once you set a password with an instance it will
26 #persist, even if your session is disconnected.
28 #Unfortunately there is no way to do callbacks from the server. For example, if you want to do
29 #something special when a new song begins, the best you can do is monitor MPD#currentsong.dbid for a
30 #new ID number and then do that something when you notice a change. But given latency you are
31 #unlikely to be able to stop the next song from starting. What I'd like to see is a feature added to
32 #MPD where when each song finishes it loads the next song and then waits for a "continue" signal
33 #before beginning playback. In the meantime the only way to do this would be to constantly maintain
34 #a single song playlist, swapping out the finished song for a new song each time.
36 #== Example
38 # require 'mpd'
40 # m = MPD.new('some_host')
41 # m.play                   => '256'
42 # m.next                   => '881'
43 # m.prev                   => '256'
44 # m.currentsong.title      => 'Ruby Tuesday'
45 # m.strf('%a - %t')        => 'The Beatles - Ruby Tuesday'
47 #== About
49 #mpd.rb is Copyright (c) 2004, Michael C. Libby (mcl@andsoforth.com)
50
51 #mpd.rb homepage is: http://www.andsoforth.com/geek/MPD.html
53 #report mpd.rb bugs to mcl@andsoforth.com
55 #Translated and adapted from MPD.pm by Tue Abrahamsen. 
57 #== LICENSE
59 #This program is free software; you can redistribute it and/or modify
60 #it under the terms of the GNU General Public License as published by
61 #the Free Software Foundation; either version 2 of the License, or
62 #(at your option) any later version.
64 #This program is distributed in the hope that it will be useful,
65 #but WITHOUT ANY WARRANTY; without even the implied warranty of
66 #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
67 #GNU General Public License for more details.
69 #You should have received a copy of the GNU General Public License
70 #along with this program; if not, write to the Free Software
71 #Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
73 #See file COPYING for details.
75 class MPD
76   MPD_VERSION = '0.11.5' #Version of MPD this version of mpd.rb was tested against
77   VERSION = '0.2.1'
78   DEFAULT_MPD_HOST = 'localhost'
79   DEFAULT_MPD_PORT = 6600
81   # MPD::SongInfo elements are:
82   #
83   # +file+ :: full pathname of file as seen by server
84   # +album+ :: name of the album
85   # +artist+ :: name of the artist
86   # +dbid+ :: mpd db id for track
87   # +pos+ :: playlist array index (starting at 0)
88   # +time+ :: time of track in seconds
89   # +title+ :: track title
90   # +track+ :: track number within album
91   #
92   SongInfo = Struct.new("SongInfo", "file", "album", "artist", "dbid", "pos", "time", "title", "track")
94   # MPD::Error elements are:
95   #
96   # +number+      :: ID number of the error as Integer
97   # +index+       :: Line number of the error (0 if not in a command list) as Integer
98   # +command+     :: Command name that caused the error
99   # +description+ :: Human readable description of the error
100   #
101   Error = Struct.new("Error", "number", "index", "command", "description")
103   #common regexps precompiled for speed and clarity
104   #
105   @@re = {
106     'ACK_MESSAGE'    => Regexp.new(/^ACK \[(\d+)\@(\d+)\] \{(.+)\} (.+)$/),
107     'DIGITS_ONLY'    => Regexp.new(/^\d+$/),
108     'OK_MPD_VERSION' => Regexp.new(/^OK MPD (.+)$/),  
109     'NON_DIGITS'     => Regexp.new(/^\D+$/),
110     'LISTALL'        => Regexp.new(/^file:\s/),
111     'PING'           => Regexp.new(/^OK/),
112     'PLAYLIST'       => Regexp.new(/^(\d+?):(.+)$/),
113     'PLAYLISTINFO'   => Regexp.new(/^(.+?):\s(.+)$/),
114     'STATS'          => Regexp.new(/^(.+?):\s(.+)$/),
115     'STATUS'         => Regexp.new(/^(.+?):\s(.+)$/),
116   }
118   # If the user has environment variables MPD_HOST or MPD_PORT set, these will override the default
119   # settings. Setting host or port in MPD.new will override both the default and the user settings.
120   # Defaults are defined in class constants MPD::DEFAULT_MPD_HOST and MPD::DEFAULT_MPD_PORT.
121   #
122   def initialize(mpd_host = nil, mpd_port = nil)
123     #behavior-related
124     @overwrite_playlist = true
125     @allow_toggle_states = true
126     @debug_socket = false
127   
128     @mpd_host = mpd_host
129     @mpd_host = ENV['MPD_HOST'] if @mpd_host.nil?
130     @mpd_host = DEFAULT_MPD_HOST if @mpd_host.nil?
132     @mpd_port = mpd_port
133     @mpd_port = ENV['MPD_PORT'] if @mpd_port.nil?
134     @mpd_port = DEFAULT_MPD_PORT if @mpd_port.nil?
136     @socket = nil
137     @mpd_version = nil
138     @password = nil
139     @error = nil
140   end
142   # Add song at <i>path</i> to the playlist. <i>path</i> is the relative path as seen by the server,
143   # not the actual path name of the file on the filesystem.
144   #
145   def add(path)
146     socket_puts("add \"#{path}\"")
147   end
149   # Clear the playlist of all entries. Consider MPD#save first.
150   #
151   def clear
152     socket_puts("clear")
153   end
155   # Clear the error element in status info. 
156   # Rare that you will need or want to do this. Most error info is cleared automatically anytime a
157   # valid play type command is issued or continues to function.
158   #
159   def clearerror
160     @error = nil
161     socket_puts("clearerror")
162   end
163   
164   # Close the connection to the server. 
165   #
166   def close
167     return nil unless is_connected?
168     socket_puts("close")
169     @socket = nil
170   end
172   # Private method for creating command lists.
173   #
174   def command_list_begin
175     @command_list = ["command_list_begin"]
176   end
178   # Wish this would take a block, but haven't quite figured out to get that to work
179   # For now just put commands in the list.
180   #
181   def command(cmd)
182     @command_list << cmd
183   end
185   # Closes and executes a command list.
186   #
187   def command_list_end
188     @command_list << "command_list_end"
189     sp = @command_list.flatten.join("\n")
190     @command_list = []
191     socket_puts(sp)
192   end
194   # Activate a closed connection. Will automatically send password if one has been set.
195   #
196   def connect
197     unless is_connected? then
198       warn "connecting to socket" if @debug_socket
199       @socket = TCPSocket.new(@mpd_host, @mpd_port)
200       if md = @@re['OK_MPD_VERSION'].match(@socket.readline) then
201         @mpd_version = md[1]
202         if @mpd_version > MPD_VERSION then
203           warn "MPD server version newer than mpd.rb version - expect the unexpected" 
204         end
205         unless @password.nil? then
206           warn "connect sending password" if @debug_socket
207           @socket.puts("password #{@password}")
208           get_server_response
209         end
210       else
211         warn "Connection error (Invalid Version Response)"
212       end
213     end
214     return true
215   end
217   # Clear every entry from the playlist but the current song.
218   #
219   def crop
220     # this really ought to just generate a list and send that to delete()
221     command_list_begin
222     (playlistlength.to_i - 1).downto(currentsong.pos + 1) do |i|
223       command( "delete #{i}" )
224     end
225     (currentsong.pos - 1).downto(0) do |i|
226       command( "delete #{i}" )
227     end
228     command_list_end
229   end
231   # Sets the crossfade value (in seconds)
232   #
233   def crossfade(fade_value)
234     socket_puts("crossfade #{fade_value}")
235     status['xfade']
236   end
238   # Returns an instance of Struct MPD::SongInfo.
239   #
240   def currentsong
241     response_to_songinfo(@@re['PLAYLISTINFO'],
242                          socket_puts("currentsong")
243                          )[0]
244   end
246   # Turns off socket command debugging.
247   #
248   def debug_off
249     @debug_socket = false
250   end
252   # Turns on socket command debugging (prints each socket command to STDERR as well as the socket)
253   #
254   def debug_on
255     @debug_socket = true
256   end
258   # <i>song</i> is one of:
259   # * a song's playlist number,
260   # * a song's MPD database ID (if <i>from_id</i> is set to true),
261   # * any object that implements a <i>collect</i> function that ultimately boils down to a set of integers. :)
262   # 
263   # Examples:
264   # <tt>MPD#delete(1)                  # delete second song (remember playlist starts at index 0)</tt>
265   # <tt>MPD#delete(0..4)               # delete first five songs</tt>
266   # <tt>MPD#delete(['1', '2', '3'])    # delete songs two, three, and four</tt>
267   # <tt>MPD#delete(1..3, 45..48, '99') # delete songs two thru four, forty-six thru forty-nine, and one hundred
268   #
269   # When <i>from_id</i> is true, the argument(s) will be treated as MPD database IDs.
270   # It is not recommended to use ranges with IDs since they are unlikely to be consecutive.
271   # An array of IDs, however, would be handy. And don't worry about using indexes in a long list.
272   # The function will convert all references to IDs before deleting (as well as removing duplicates).
273   def delete(song, from_id = false)
274     cmd = from_id ? 'deleteid' : 'delete'
275     slist = expand_list(song).flatten.uniq
277     if slist.length == 1 then
278       return nil unless @@re['DIGITS_ONLY'].match(slist[0].to_s)
279       return socket_puts("#{cmd} #{slist[0]}")
280     else
281       unless from_id then
282         # convert to ID for list commands, otherwise as soon as first delete happens
283         # the rest of the indexes won't be accurate
284         slist = slist.map{|x| playlistinfo(x).dbid }
285       end
286       command_list_begin
287       slist.each do |x|
288         next unless @@re['DIGITS_ONLY'].match(slist[0].to_s)
289         command("deleteid #{x}")
290       end
291       return command_list_end
292     end
293   end
295   # Returns a Struct MPD::Error,
296   #
297   def error
298     @error
299   end
301   # Alias for MPD#delete(song_id, true)
302   def deleteid(song_id)
303     delete(song_id, true)
304   end
305   
306   # Takes and prepares any <i>collect</i>able list to be flattened and uniq'ed.
307   # That is, it converts <tt>[0..2, '3', [4, 5]]</tt> into <tt>[0, 1, 2, '3', [4, 5]]</tt>.
308   # Essentially it expands Range objects and the like.
309   #
310   def expand_list(d)
311     if d.respond_to?("collect") then
312       if d.collect == d then
313         return d.collect{|x| expand_list(x)}
314       else
315         dc = d.collect
316         if dc.length > 1 then
317           return d.collect{|x| expand_list(x)}
318         else
319           return [d]
320         end
321       end
322     else
323       return [d]
324     end
325   end
327   # Finds exact matches of <i>find_string</i> in the MPD database.
328   # <i>find_type</i> is limited to 'album', 'artist', and 'title'.
329   #
330   # Returns an array containing an instance of MPD::SongInfo (Struct) for every song in the current
331   # playlist.
332   #
333   # Results from MPD#find() do not have valid information for dbid or pos
334   #
335   def find(find_type, find_string)
336     response_to_songinfo(@@re['PLAYLISTINFO'],
337                          socket_puts("find #{find_type} \"#{find_string}\"")
338                          )
339   end
341   # Runs MPD#find using the given parameters and automatically adds each result
342   # to the playlist. Returns an Array of MPD::SongInfo structs.
343   #
344   def find_add(find_type, find_string)
345     flist = find(find_type, find_string)
346     command_list_begin
347     flist.each do |x|
348       command("add #{x.file}")
349     end
350     command_list_end
351     flist
352   end
354   # Private method for handling the messages the server sends. 
355   #
356   def get_server_response
357     response = []
358     while line = @socket.readline.chomp do
359       # Did we cause an error? Save the data!
360       if md = @@re['ACK_MESSAGE'].match(line) then
361         @error = Error.new(md[1].to_i, md[2].to_i, md[3], md[4])
362         raise "MPD Error #{md[1]}: #{md[4]}"
363       end
364       return response if @@re['PING'].match(line)
365       response << line
366     end
367     return response
368   end
370   # Internal method for converting results from currentsong, playlistinfo, playlistid to
371   # MPD::SongInfo structs
372   #
373   def hash_to_songinfo(h)
374     SongInfo.new(h['file'],
375                  h['Album'],
376                  h['Artist'],
377                  h['Id'].nil? ? nil : h['Id'].to_i, 
378                  h['Pos'].nil? ? nil : h['Pos'].to_i, 
379                  h['Time'],
380                  h['Title'],
381                  h['Track']
382                  )
383   end
385   # Pings the server and returns true or false depending on whether a response was receieved.
386   #
387   def is_connected?
388     return false if @socket.nil? || @socket.closed?
389     warn "is_connected to socket: ping" if @debug_socket
390     @socket.puts("ping")
391     if @@re['PING'].match(@socket.readline) then
392       return true
393     end
394     return false
395   rescue
396     return false
397   end
398   
399   # Kill the MPD server.
400   # No way exists to restart it from here, so be careful.
401   #
402   def kill
403     socket_puts("kill")
404   rescue #kill always causes a readline error in get_server_response
405     @error = nil
406   end
408   # Gets a list of Artist names or Album names from the MPD database (not the current playlist).
409   # <i>type</i> is either 'artist' (default) or 'album'. The <i>artist</i> parameter is
410   # used with <i>type</i>='album' to limit results to just the albums by that artist.
411   #
412   def list(type = 'artist', artist = '')
413     response = socket_puts(type == 'album' ? "list album \"#{artist}\"" : "list artist")
414     tmp = []
415     response.each do |f|
416       if md = /^(?:Artist|Album):\s(.+)$/.match(f) then
417         tmp << md[1]
418       end
419     end
420     return tmp
421   end
423   # Returns a list of all filenames in <i>path</i> (recursively) according to the MPD database.
424   # If <i>path</i> is omitted, lists every file in the database.
425   #
426   def listall(path = '')
427     resp = socket_puts("listall \"#{path}\"").grep(@@re['LISTALL']).map{|x| x.sub(@@re['LISTALL'], '')}
428     resp.compact
429   end
431   # Returns an Array containing MPD::SongInfo for each file in <i>path</i> (recursively) according
432   # to the MPD database.
433   # If <i>path</i> is omitted, lists every file in the datbase.
434   def listallinfo(path = '')
435     results = []
436     hash = {}
437     response_to_songinfo(@@re['PLAYLISTINFO'],
438                          socket_puts("listallinfo \"#{path}\"")
439                          )
440   end
442   # Load a playlist from the MPD playlist directory.
443   #
444   def load(playlist)
445     socket_puts("load \"#{playlist}\"")
446     status['playlistid']
447   end
449   # Returns Array of strings containing a list of directories, files or playlists in <i>path</i> (as
450   # seen by the MPD database).  
451   # If <i>path</i> is omitted, uses the root directory.
452   def lsinfo(path = '')
453     results = []
454     element = {}
455     socket_puts("lsinfo \"#{path}\"").each do |f|
456       if md = /^(.[^:]+):\s(.+)$/.match(f)
457         if ['file', 'playlist', 'directory'].grep(md[1]).length > 0 then
458           results.push(f)
459         end
460       end
461     end
462     return results
463   end
466   # Returns an Array of playlist paths (as seen by the MPD database).
467   #
468   def lsplaylists
469     lsinfo.grep(/^playlist:\s/).map{|x| x.sub(/^playlist:\s/, '')}.compact
470   end
472   # Move song at <i>curr_pos</i> to <i>new_pos</i> in the playlist.
473   #
474   def move(curr_pos, new_pos)
475     socket_puts("move #{curr_pos} #{new_pos}")
476   end
478   # Move song with MPD database ID <i>song_id</i> to <i>new_pos</i> in the playlist.
479   #
480   def moveid(song_id, new_pos)
481     socket_puts("moveid #{song_id} #{new_pos}")
482   end
484   # Return the version string returned by the MPD server
485   #
486   def mpd_version
487     @mpd_version
488   end
490   # Play next song in the playlist. See note about shuffling in MPD#set_random
491   # Returns songid as Integer.
492   #
493   def next 
494     socket_puts("next")
495     currentsong
496   end
498   # Send the password <i>pass</i> to the server and sets it for this MPD instance. 
499   # If <i>pass</i> is omitted, uses any previously set password (see MPD#password=).
500   # Once a password is set by either method MPD#connect can automatically send the password if
501   # disconnected.  
502   #
503   def password(pass = @password)
504     @password = pass
505     socket_puts("password #{pass}")
506   end
507   
508   # Set the password to <i>pass</i>.
509   def password=(pass)
510     @password = pass
511   end
513   # Pause playback on the server
514   # Returns ('pause'|'play'|'stop'). 
515   #
516   def pause(value = nil)
517     cstatus = status['state']
518     return cstatus if cstatus == 'stop'
520     if value.nil? && @allow_toggle_states then
521       value = cstatus == 'pause' ? '0' : '1'
522     end
523     socket_puts("pause #{value}")
524     status['state']
525   end
527   # Send a ping to the server and keep the connection alive.
528   #
529   def ping
530     socket_puts("ping")
531   end
532   
533   # Start playback of songs in the playlist with song at index 
534   # <i>number</i> in the playlist.
535   # Empty <i>number</i> starts playing from current spot or beginning.
536   # Returns current song as MPD::SongInfo.
537   #
538   def play(number = '')
539     socket_puts("play #{number}")
540     currentsong
541   end
543   # Start playback of songs in the playlist with song having 
544   # mpd database ID <i>number</i>.
545   # Empty <i>number</i> starts playing from current spot or beginning.
546   # Returns songid as Integer.
547   #
548   def playid(number = '')
549     socket_puts("playid #{number}")
550     status['songid']
551   end
553   # <b>Deprecated</b> Use MPD#playlistinfo or MPD#playlistid instead
554   # Returns an Array containing paths for each song in the current playlist
555   #
556   def playlist
557     warn "MPD#playlist is deprecated. Use MPD#playlistinfo or MPD#playlistid instead."
558     plist = []
559     socket_puts("playlist").each do |f|
560       if md = @@re['PLAYLIST'].match(f) then
561         plist << md[2]
562       end
563     end
564     plist
565   end
567   # Returns an array containing an instance of MPD::SongInfo (Struct) for every song in the current
568   # playlist or a single instance of MPD::SongInfo (if <i>snum</i> is specified).
569   #
570   # <i>snum</i> is the song's index in the playlist.
571   # If <i>snum</i> == '' then the whole playlist is returned.
572   def playlistinfo(snum = '', from_id = false)
573     plist = response_to_songinfo(@@re['PLAYLISTINFO'],
574                                  socket_puts("playlist#{from_id ? 'id' : 'info'} #{snum}")
575                                  )
576     return snum == '' ? plist : plist[0]
577   end
579   # An alias for MPD#playlistinfo with <i>from_id</i> = true.
580   # Looks up song <i>sid</i> is the song's MPD ID (<i>dbid</i> in an MPD::SongInfo
581   # instance).
582   # Returns an Array of Hashes.
583   #
584   def playlistid(sid = '')
585     playlistinfo(sid, true)
586   end
587   
588   # Get the length of the playlist from the server.
589   # Returns an Integer
590   #
591   def playlistlength
592     status['playlistlength'].to_i
593   end
595   # Returns an Array of MPD#SongInfo. The songs listed are either those added since previous
596   # playlist version, <i>playlist_num</i>, <b>or</b>, if a song was deleted, the new playlist that
597   # resulted. Cumbersome. Eventually methods will be written that help track adds/deletes better.
598   #
599   def plchanges(playlist_num = '-1')
600     response_to_songinfo(@@re['PLAYLISTINFO'],
601                          socket_puts("plchanges #{playlist_num}")
602                          )
603   end
605   # Play previous song in the playlist. See note about shuffling in MPD#set_random.
606   # Return songid as Integer
607   #
608   def previous
609     socket_puts("previous")
610     currentsong
611   end
612   alias prev previous
614   # Sets random mode on the server, either directly, or by toggling (if
615   # no argument given and @allow_toggle_states = true). Mode "0" = not 
616   # random; Mode "1" = random. Random affects playback order, but not playlist
617   # order. When random is on the playlist is shuffled and then used instead
618   # of the actual playlist. Previous and next in random go to the previous
619   # and next songs in the shuffled playlist. Calling MPD#next and then 
620   # MPD#prev would start playback at the beginning of the current song.
621   #
622   def random(mode = nil)
623     return nil if mode.nil? && !@allow_toggle_states
624     return nil unless /^(0|1)$/.match(mode) || @allow_toggle_states
625     if mode.nil? then
626       mode = status['random'] == '1' ? '0' : '1'                                               
627     end
628     socket_puts("random #{mode}")
629     status['random']
630   end
631   
632   # Sets repeat mode on the server, either directly, or by toggling (if
633   # no argument given and @allow_toggle_states = true). Mode "0" = not 
634   # repeat; Mode "1" = repeat. Repeat means that server will play song 1
635   # when it reaches the end of the playlist.
636   #
637   def repeat(mode = nil)
638     return nil if mode.nil? && !@allow_toggle_states
639     return nil unless /^(0|1)$/.match(mode) || @allow_toggle_states
640     if mode.nil? then
641       mode = status['repeat'] == '1' ? '0' : '1'
642     end
643     socket_puts("repeat #{mode}")
644     status['repeat']
645   end
647   # Private method to convert playlistinfo style server output into MPD#SongInfo list
648   # <i>re</i> is the Regexp to use to match "<element type>: <element>".
649   # <i>response</i> is the output from MPD#socket_puts.
650   def response_to_songinfo(re, response)
651     list = []
652     hash = {}
653     response.each do |f|
654       if md = re.match(f) then
655         if md[1] == 'file' then
656           if hash == {} then
657             list << nil unless list == []
658           else
659             list << hash_to_songinfo(hash)
660           end
661           hash = {}
662         end
663         hash[md[1]] = md[2]
664       end
665     end
666     if hash == {} then
667       list << nil unless list == []
668     else
669       list << hash_to_songinfo(hash)
670     end
671     return list
672   end
674   # Deletes the playlist file <i>playlist</i>.m3u from the playlist directory on the server.
675   #
676   def rm(playlist)
677     socket_puts("rm \"#{playlist}\"")
678   end
680   # Save the current playlist as <i>playlist</i>.m3u in the playlist directory on the server.
681   # If <i>force</i> is true, any existing playlist with the same name will be deleted before saving.
682   #
683   def save(playlist, force = @overwrite_playlist)
684     socket_puts("save \"#{playlist}\"")
685   rescue
686     if error.number == 56 && force then
687       rm(playlist)
688       return socket_puts("save \"#{playlist}\"")
689     end
690     raise
691   end
693   # Similar to MPD#find, only search is not strict. It will match <i>search_type</i> of 'artist',
694   # 'album', 'title', or 'filename' against <i>search_string</i>.
695   # Returns an Array of MPD#SongInfo.
696   #
697   def search(search_type, search_string)
698     response_to_songinfo(@@re['PLAYLISTINFO'],
699                          socket_puts("search #{search_type} \"#{search_string}\"")
700                          )
701   end
702   
703   # Conducts a search of <i>search_type</i> for <i>search_string</i> and adds the results to the
704   # current playlist. Returns the results of the search.
705   #
706   def search_add(search_type, search_string)
707     results = search(search_type, search_string)
708     unless results == [] then
709       command_list_begin
710       results.each do |s|
711         command( "add \"#{s.file}\"")
712       end
713       command_list_end
714     end
715     return results
716   end
718   # Seek to <i>position</i> seconds within song number <i>song</i> in the playlist. If no
719   # <i>song</i> is given, uses current song.  
720   #
721   def seek(position, song = currentsong.pos)
722     socket_puts("seek #{song} #{position}")
723   end
725   # Seek to <i>position</i> seconds within song ID <i>song</i>. If no <i>song</i> is given, uses
726   # current song.  
727   #
728   def seekid(position, song_id = currentsong.dbid)
729     socket_puts("seekid #{song_id} #{position}")
730   end
732   # Set the volume to <i>volume</i>. Range is limited to 0-100. MPD#set_volume 
733   # will adjust any value passed less than 0 or greater than 100.
734   #
735   def setvol(vol)
736     vol = 0 if vol.to_i < 0
737     vol = 100 if vol.to_i > 100
738     socket_puts("setvol #{vol}")
739     status['volume']
740   end
742   # Shuffles the current playlist and increments playlist version by 1.
743   # This will rearrange your actual playlist with no way to resort it 
744   # (other than saving it before shuffling and then reloading it).
745   # If you just want random playback use MPD#random.
746   #
747   def shuffle
748     socket_puts("shuffle")
749   end
751   # Sends a command to the MPD server and optionally to STDOUT if
752   # MPD#debug_on has been used to turn debugging on
753   #
754   def socket_puts(cmd)
755     connect unless is_connected?
756     warn "socket_puts to socket: #{cmd}" if @debug_socket
757     @socket.puts(cmd)
758     return get_server_response
759   end
761   # Returns a hash containing various server stats:
762   #
763   # +albums+ :: number of albums in mpd database
764   # +artists+ :: number of artists in mpd database
765   # +db_playtime+ :: sum of all song times in in mpd database
766   # +db_update+ :: last mpd database update in UNIX time
767   # +playtime+ :: time length of music played during uptime
768   # +songs+ :: number of songs in mpd database
769   # +uptime+ :: mpd server uptime in seconds
770   #
771   def stats
772     s = {}
773     socket_puts("stats").each do |f|
774       if md = @@re['STATS'].match(f);
775         s[md[1]] = md[2] 
776       end
777     end
778     return s
779   end
781   # Returns a hash containing various status elements:
782   #
783   # +audio+ :: '<sampleRate>:<bits>:<channels>' describes audio stream
784   # +bitrate+ :: bitrate of audio stream in kbps
785   # +error+ :: if there is an error, returns message here
786   # +playlist+ :: the playlist version number as String
787   # +playlistlength+ :: number indicating the length of the playlist as String
788   # +repeat+ :: '0' or '1'
789   # +song+ :: playlist index number of current song (stopped on or playing)
790   # +songid+ :: song ID number of current song (stopped on or playing)
791   # +state+ :: 'pause'|'play'|'stop'
792   # +time+ :: '<elapsed>:<total>' (both in seconds) of current playing/paused song
793   # +updating_db+ :: '<job id>' if currently updating db
794   # +volume+ :: '0' to '100'
795   # +xfade+ :: crossfade in seconds
796   #
797   def status
798     s = {}
799     socket_puts("status").each do |f|
800       if md = @@re['STATUS'].match(f) then
801         s[md[1]] = md[2]
802       end
803     end
804     return s
805   end
806   
807   # Stops playback.
808   # Returns ('pause'|'play'|'stop').
809   #
810   def stop
811     socket_puts("stop")
812     status['state']
813   end
815   # Pass a format string (like strftime) and get back a string of MPD information.
816   #
817   # Format string elements are: 
818   # <tt>%f</tt> :: filename
819   # <tt>%a</tt> :: artist
820   # <tt>%A</tt> :: album
821   # <tt>%i</tt> :: MPD database ID
822   # <tt>%p</tt> :: playlist position
823   # <tt>%t</tt> :: title
824   # <tt>%T</tt> :: track time (in seconds)
825   # <tt>%n</tt> :: track number
826   # <tt>%e</tt> :: elapsed playtime (MM:SS form)
827   # <tt>%l</tt> :: track length (MM:SS form)
828   #
829   # <i>song_info</i> can either be an existing MPD::SongInfo object (such as the one returned by
830   # MPD#currentsong) or the MPD database ID for a song. If no <i>song_info</i> is given, all
831   # song-related elements will come from the current song.
832   #
833   def strf(format_string, song_info = currentsong) 
834     unless song_info.class == Struct::SongInfo
835       if @@re['DIGITS_ONLY'].match(song_info.to_s) then
836         song_info = playlistid(song_info)
837       end
838     end
840     s = ''
841     format_string.scan(/%[EO]?.|./o) do |x|
842       case x
843       when '%f'
844         s << song_info.file.to_s
846       when '%a'
847         s << song_info.artist.to_s
849       when '%A'
850         s << song_info.album.to_s
852       when '%i'
853         s << song_info.dbid.to_s
855       when '%p'
856         s << song_info.pos.to_s
857         
858       when '%t'
859         s << song_info.title.to_s
861       when '%T'
862         s << song_info.time.to_s
864       when '%n'
865         s << song_info.track.to_s
867       when '%e'
868         t = status['time'].split(/:/)[0].to_f
869         s << sprintf( "%d:%02d", t / 60, t % 60 )
871       when '%l'
872         t = status['time'].split(/:/)[1].to_f
873         s << sprintf( "%d:%02d", t / 60, t % 60 )
875       else
876         s << x.to_s
878       end
879     end
880     return s
881   end
883   # Swap two songs in the playlist, either based on playlist indexes or song IDs (when <i>from_id</i> is true).
884   #
885   def swap(song_from, song_to, from_id = false)
886     if @@re['DIGITS_ONLY'].match(song_from.to_s) && @@re['DIGITS_ONLY'].match(song_to.to_s) then
887       return socket_puts("#{from_id ? 'swapid' : 'swap'} #{song_from} #{song_to}")
888     else 
889       raise "invalid input for swap"
890     end
891   end
892   
893   # Alias for MPD#swap(song_id_from, song_id_to, true)
894   #
895   def swap_id(song_id_from, song_id_to)
896     swap(song_id_from, song_id_to, true)
897   end
899   # Searches MP3 directory for new music and removes old music from the MPD database.
900   # <i>path</i> is an optional argument that specifies a particular directory or 
901   # song/file to update. <i>path</i> can also be a list of paths to update.
902   # If <i>path</i> is omitted, the entire database will be updated using the server's 
903   # base MP3 directory.
904   #
905   def update(path = '')
906     ulist = expand_list(path).flatten.uniq
907     if ulist.length == 1 then
908       return socket_puts("update #{ulist[0]}")
909     else
910       command_list_begin
911       ulist.each do |x|
912         command("update #{x}")
913       end
914       return command_list_end
915     end
916   end
917   
918   # Returns the types of URLs that can be handled by the server.
919   #
920   def urlhandlers
921     handlers = []
922     socket_puts("urlhandlers").each do |f|
923       handlers << f if /^handler: (.+)$/.match(f)
924     end
925     return handlers
926   end
928   # <b>Deprecated</b> Use MPD#setvol instead.
929   # Increase or decrease volume (depending on whether <i>vol_change</i> is positive or
930   # negative. Volume is limited to the range of 0-100 (server ensures that change
931   # does not take volume out of range).
932   # Returns volume.
933   #
934   def volume(vol_change)
935     warn "MPD#volume is deprecated. Use MPD#setvol instead."
936     socket_puts("volume #{vol_change}")
937     status['volume']
938   end
940   private :command, :command_list_begin, :command_list_end, :expand_list
941   private :connect, :get_server_response, :socket_puts
942   private :hash_to_songinfo, :response_to_songinfo