added README
[mpdhero.git] / mpd.rb
blobe07a7ab0a3ba8bdbb2a85fbcd7fbf1af1f5b6eb0
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          puts response
368     return response
369   end
371   # Internal method for converting results from currentsong, playlistinfo, playlistid to
372   # MPD::SongInfo structs
373   #
374   def hash_to_songinfo(h)
375     SongInfo.new(h['file'],
376                  h['Album'],
377                  h['Artist'],
378                  h['Id'].nil? ? nil : h['Id'].to_i, 
379                  h['Pos'].nil? ? nil : h['Pos'].to_i, 
380                  h['Time'],
381                  h['Title'],
382                  h['Track']
383                  )
384   end
386   # Pings the server and returns true or false depending on whether a response was receieved.
387   #
388   def is_connected?
389     return false if @socket.nil? || @socket.closed?
390     warn "is_connected to socket: ping" if @debug_socket
391     @socket.puts("ping")
392     if @@re['PING'].match(@socket.readline) then
393       return true
394     end
395     return false
396   rescue
397     return false
398   end
399   
400   # Kill the MPD server.
401   # No way exists to restart it from here, so be careful.
402   #
403   def kill
404     socket_puts("kill")
405   rescue #kill always causes a readline error in get_server_response
406     @error = nil
407   end
409   # Gets a list of Artist names or Album names from the MPD database (not the current playlist).
410   # <i>type</i> is either 'artist' (default) or 'album'. The <i>artist</i> parameter is
411   # used with <i>type</i>='album' to limit results to just the albums by that artist.
412   #
413   def list(type = 'artist', artist = '')
414     response = socket_puts(type == 'album' ? "list album \"#{artist}\"" : "list artist")
415     tmp = []
416     response.each do |f|
417       if md = /^(?:Artist|Album):\s(.+)$/.match(f) then
418         tmp << md[1]
419       end
420     end
421     return tmp
422   end
424   # Returns a list of all filenames in <i>path</i> (recursively) according to the MPD database.
425   # If <i>path</i> is omitted, lists every file in the database.
426   #
427   def listall(path = '')
428     resp = socket_puts("listall \"#{path}\"").grep(@@re['LISTALL']).map{|x| x.sub(@@re['LISTALL'], '')}
429     resp.compact
430   end
432   # Returns an Array containing MPD::SongInfo for each file in <i>path</i> (recursively) according
433   # to the MPD database.
434   # If <i>path</i> is omitted, lists every file in the datbase.
435   def listallinfo(path = '')
436     results = []
437     hash = {}
438     response_to_songinfo(@@re['PLAYLISTINFO'],
439                          socket_puts("listallinfo \"#{path}\"")
440                          )
441   end
443   # Load a playlist from the MPD playlist directory.
444   #
445   def load(playlist)
446     socket_puts("load \"#{playlist}\"")
447     status['playlistid']
448   end
450   # Returns Array of strings containing a list of directories, files or playlists in <i>path</i> (as
451   # seen by the MPD database).  
452   # If <i>path</i> is omitted, uses the root directory.
453   def lsinfo(path = '')
454     results = []
455     element = {}
456     socket_puts("lsinfo \"#{path}\"").each do |f|
457       if md = /^(.[^:]+):\s(.+)$/.match(f)
458         if ['file', 'playlist', 'directory'].grep(md[1]).length > 0 then
459           results.push(f)
460         end
461       end
462     end
463     return results
464   end
467   # Returns an Array of playlist paths (as seen by the MPD database).
468   #
469   def lsplaylists
470     lsinfo.grep(/^playlist:\s/).map{|x| x.sub(/^playlist:\s/, '')}.compact
471   end
473   # Move song at <i>curr_pos</i> to <i>new_pos</i> in the playlist.
474   #
475   def move(curr_pos, new_pos)
476     socket_puts("move #{curr_pos} #{new_pos}")
477   end
479   # Move song with MPD database ID <i>song_id</i> to <i>new_pos</i> in the playlist.
480   #
481   def moveid(song_id, new_pos)
482     socket_puts("moveid #{song_id} #{new_pos}")
483   end
485   # Return the version string returned by the MPD server
486   #
487   def mpd_version
488     @mpd_version
489   end
491   # Play next song in the playlist. See note about shuffling in MPD#set_random
492   # Returns songid as Integer.
493   #
494   def next 
495     socket_puts("next")
496     currentsong
497   end
499   # Send the password <i>pass</i> to the server and sets it for this MPD instance. 
500   # If <i>pass</i> is omitted, uses any previously set password (see MPD#password=).
501   # Once a password is set by either method MPD#connect can automatically send the password if
502   # disconnected.  
503   #
504   def password(pass = @password)
505     @password = pass
506     socket_puts("password #{pass}")
507   end
508   
509   # Set the password to <i>pass</i>.
510   def password=(pass)
511     @password = pass
512   end
514   # Pause playback on the server
515   # Returns ('pause'|'play'|'stop'). 
516   #
517   def pause(value = nil)
518     cstatus = status['state']
519     return cstatus if cstatus == 'stop'
521     if value.nil? && @allow_toggle_states then
522       value = cstatus == 'pause' ? '0' : '1'
523     end
524     socket_puts("pause #{value}")
525     status['state']
526   end
528   # Send a ping to the server and keep the connection alive.
529   #
530   def ping
531     socket_puts("ping")
532   end
533   
534   # Start playback of songs in the playlist with song at index 
535   # <i>number</i> in the playlist.
536   # Empty <i>number</i> starts playing from current spot or beginning.
537   # Returns current song as MPD::SongInfo.
538   #
539   def play(number = '')
540     socket_puts("play #{number}")
541     currentsong
542   end
544   # Start playback of songs in the playlist with song having 
545   # mpd database ID <i>number</i>.
546   # Empty <i>number</i> starts playing from current spot or beginning.
547   # Returns songid as Integer.
548   #
549   def playid(number = '')
550     socket_puts("playid #{number}")
551     status['songid']
552   end
554   # <b>Deprecated</b> Use MPD#playlistinfo or MPD#playlistid instead
555   # Returns an Array containing paths for each song in the current playlist
556   #
557   def playlist
558     warn "MPD#playlist is deprecated. Use MPD#playlistinfo or MPD#playlistid instead."
559     plist = []
560     socket_puts("playlist").each do |f|
561       if md = @@re['PLAYLIST'].match(f) then
562         plist << md[2]
563       end
564     end
565     plist
566   end
568   # Returns an array containing an instance of MPD::SongInfo (Struct) for every song in the current
569   # playlist or a single instance of MPD::SongInfo (if <i>snum</i> is specified).
570   #
571   # <i>snum</i> is the song's index in the playlist.
572   # If <i>snum</i> == '' then the whole playlist is returned.
573   def playlistinfo(snum = '', from_id = false)
574     plist = response_to_songinfo(@@re['PLAYLISTINFO'],
575                                  socket_puts("playlist#{from_id ? 'id' : 'info'} #{snum}")
576                                  )
577     return snum == '' ? plist : plist[0]
578   end
580   # An alias for MPD#playlistinfo with <i>from_id</i> = true.
581   # Looks up song <i>sid</i> is the song's MPD ID (<i>dbid</i> in an MPD::SongInfo
582   # instance).
583   # Returns an Array of Hashes.
584   #
585   def playlistid(sid = '')
586     playlistinfo(sid, true)
587   end
588   
589   # Get the length of the playlist from the server.
590   # Returns an Integer
591   #
592   def playlistlength
593     status['playlistlength'].to_i
594   end
596   # Returns an Array of MPD#SongInfo. The songs listed are either those added since previous
597   # playlist version, <i>playlist_num</i>, <b>or</b>, if a song was deleted, the new playlist that
598   # resulted. Cumbersome. Eventually methods will be written that help track adds/deletes better.
599   #
600   def plchanges(playlist_num = '-1')
601     response_to_songinfo(@@re['PLAYLISTINFO'],
602                          socket_puts("plchanges #{playlist_num}")
603                          )
604   end
606   # Play previous song in the playlist. See note about shuffling in MPD#set_random.
607   # Return songid as Integer
608   #
609   def previous
610     socket_puts("previous")
611     currentsong
612   end
613   alias prev previous
615   # Sets random mode on the server, either directly, or by toggling (if
616   # no argument given and @allow_toggle_states = true). Mode "0" = not 
617   # random; Mode "1" = random. Random affects playback order, but not playlist
618   # order. When random is on the playlist is shuffled and then used instead
619   # of the actual playlist. Previous and next in random go to the previous
620   # and next songs in the shuffled playlist. Calling MPD#next and then 
621   # MPD#prev would start playback at the beginning of the current song.
622   #
623   def random(mode = nil)
624     return nil if mode.nil? && !@allow_toggle_states
625     return nil unless /^(0|1)$/.match(mode) || @allow_toggle_states
626     if mode.nil? then
627       mode = status['random'] == '1' ? '0' : '1'                                               
628     end
629     socket_puts("random #{mode}")
630     status['random']
631   end
632   
633   # Sets repeat mode on the server, either directly, or by toggling (if
634   # no argument given and @allow_toggle_states = true). Mode "0" = not 
635   # repeat; Mode "1" = repeat. Repeat means that server will play song 1
636   # when it reaches the end of the playlist.
637   #
638   def repeat(mode = nil)
639     return nil if mode.nil? && !@allow_toggle_states
640     return nil unless /^(0|1)$/.match(mode) || @allow_toggle_states
641     if mode.nil? then
642       mode = status['repeat'] == '1' ? '0' : '1'
643     end
644     socket_puts("repeat #{mode}")
645     status['repeat']
646   end
648   # Private method to convert playlistinfo style server output into MPD#SongInfo list
649   # <i>re</i> is the Regexp to use to match "<element type>: <element>".
650   # <i>response</i> is the output from MPD#socket_puts.
651   def response_to_songinfo(re, response)
652     list = []
653     hash = {}
654     response.each do |f|
655       if md = re.match(f) then
656         if md[1] == 'file' then
657           if hash == {} then
658             list << nil unless list == []
659           else
660             list << hash_to_songinfo(hash)
661           end
662           hash = {}
663         end
664         hash[md[1]] = md[2]
665       end
666     end
667     if hash == {} then
668       list << nil unless list == []
669     else
670       list << hash_to_songinfo(hash)
671     end
672     return list
673   end
675   # Deletes the playlist file <i>playlist</i>.m3u from the playlist directory on the server.
676   #
677   def rm(playlist)
678     socket_puts("rm \"#{playlist}\"")
679   end
681   # Save the current playlist as <i>playlist</i>.m3u in the playlist directory on the server.
682   # If <i>force</i> is true, any existing playlist with the same name will be deleted before saving.
683   #
684   def save(playlist, force = @overwrite_playlist)
685     socket_puts("save \"#{playlist}\"")
686   rescue
687     if error.number == 56 && force then
688       rm(playlist)
689       return socket_puts("save \"#{playlist}\"")
690     end
691     raise
692   end
694   # Similar to MPD#find, only search is not strict. It will match <i>search_type</i> of 'artist',
695   # 'album', 'title', or 'filename' against <i>search_string</i>.
696   # Returns an Array of MPD#SongInfo.
697   #
698   def search(search_type, search_string)
699     response_to_songinfo(@@re['PLAYLISTINFO'],
700                          socket_puts("search #{search_type} \"#{search_string}\"")
701                          )
702   end
703   
704   # Conducts a search of <i>search_type</i> for <i>search_string</i> and adds the results to the
705   # current playlist. Returns the results of the search.
706   #
707   def search_add(search_type, search_string)
708     results = search(search_type, search_string)
709     unless results == [] then
710       command_list_begin
711       results.each do |s|
712         command( "add \"#{s.file}\"")
713       end
714       command_list_end
715     end
716     return results
717   end
719   # Seek to <i>position</i> seconds within song number <i>song</i> in the playlist. If no
720   # <i>song</i> is given, uses current song.  
721   #
722   def seek(position, song = currentsong.pos)
723     socket_puts("seek #{song} #{position}")
724   end
726   # Seek to <i>position</i> seconds within song ID <i>song</i>. If no <i>song</i> is given, uses
727   # current song.  
728   #
729   def seekid(position, song_id = currentsong.dbid)
730     socket_puts("seekid #{song_id} #{position}")
731   end
733   # Set the volume to <i>volume</i>. Range is limited to 0-100. MPD#set_volume 
734   # will adjust any value passed less than 0 or greater than 100.
735   #
736   def setvol(vol)
737     vol = 0 if vol.to_i < 0
738     vol = 100 if vol.to_i > 100
739     socket_puts("setvol #{vol}")
740     status['volume']
741   end
743   # Shuffles the current playlist and increments playlist version by 1.
744   # This will rearrange your actual playlist with no way to resort it 
745   # (other than saving it before shuffling and then reloading it).
746   # If you just want random playback use MPD#random.
747   #
748   def shuffle
749     socket_puts("shuffle")
750   end
752   # Sends a command to the MPD server and optionally to STDOUT if
753   # MPD#debug_on has been used to turn debugging on
754   #
755   def socket_puts(cmd)
756     connect unless is_connected?
757     warn "socket_puts to socket: #{cmd}" if @debug_socket
758     @socket.puts(cmd)
759     return get_server_response
760   end
762   # Returns a hash containing various server stats:
763   #
764   # +albums+ :: number of albums in mpd database
765   # +artists+ :: number of artists in mpd database
766   # +db_playtime+ :: sum of all song times in in mpd database
767   # +db_update+ :: last mpd database update in UNIX time
768   # +playtime+ :: time length of music played during uptime
769   # +songs+ :: number of songs in mpd database
770   # +uptime+ :: mpd server uptime in seconds
771   #
772   def stats
773     s = {}
774     socket_puts("stats").each do |f|
775       if md = @@re['STATS'].match(f);
776         s[md[1]] = md[2] 
777       end
778     end
779     return s
780   end
782   # Returns a hash containing various status elements:
783   #
784   # +audio+ :: '<sampleRate>:<bits>:<channels>' describes audio stream
785   # +bitrate+ :: bitrate of audio stream in kbps
786   # +error+ :: if there is an error, returns message here
787   # +playlist+ :: the playlist version number as String
788   # +playlistlength+ :: number indicating the length of the playlist as String
789   # +repeat+ :: '0' or '1'
790   # +song+ :: playlist index number of current song (stopped on or playing)
791   # +songid+ :: song ID number of current song (stopped on or playing)
792   # +state+ :: 'pause'|'play'|'stop'
793   # +time+ :: '<elapsed>:<total>' (both in seconds) of current playing/paused song
794   # +updating_db+ :: '<job id>' if currently updating db
795   # +volume+ :: '0' to '100'
796   # +xfade+ :: crossfade in seconds
797   #
798   def status
799     s = {}
800     socket_puts("status").each do |f|
801       if md = @@re['STATUS'].match(f) then
802         s[md[1]] = md[2]
803       end
804     end
805     return s
806   end
807   
808   # Stops playback.
809   # Returns ('pause'|'play'|'stop').
810   #
811   def stop
812     socket_puts("stop")
813     status['state']
814   end
816   # Pass a format string (like strftime) and get back a string of MPD information.
817   #
818   # Format string elements are: 
819   # <tt>%f</tt> :: filename
820   # <tt>%a</tt> :: artist
821   # <tt>%A</tt> :: album
822   # <tt>%i</tt> :: MPD database ID
823   # <tt>%p</tt> :: playlist position
824   # <tt>%t</tt> :: title
825   # <tt>%T</tt> :: track time (in seconds)
826   # <tt>%n</tt> :: track number
827   # <tt>%e</tt> :: elapsed playtime (MM:SS form)
828   # <tt>%l</tt> :: track length (MM:SS form)
829   #
830   # <i>song_info</i> can either be an existing MPD::SongInfo object (such as the one returned by
831   # MPD#currentsong) or the MPD database ID for a song. If no <i>song_info</i> is given, all
832   # song-related elements will come from the current song.
833   #
834   def strf(format_string, song_info = currentsong) 
835     unless song_info.class == Struct::SongInfo
836       if @@re['DIGITS_ONLY'].match(song_info.to_s) then
837         song_info = playlistid(song_info)
838       end
839     end
841     s = ''
842     format_string.scan(/%[EO]?.|./o) do |x|
843       case x
844       when '%f'
845         s << song_info.file.to_s
847       when '%a'
848         s << song_info.artist.to_s
850       when '%A'
851         s << song_info.album.to_s
853       when '%i'
854         s << song_info.dbid.to_s
856       when '%p'
857         s << song_info.pos.to_s
858         
859       when '%t'
860         s << song_info.title.to_s
862       when '%T'
863         s << song_info.time.to_s
865       when '%n'
866         s << song_info.track.to_s
868       when '%e'
869         t = status['time'].split(/:/)[0].to_f
870         s << sprintf( "%d:%02d", t / 60, t % 60 )
872       when '%l'
873         t = status['time'].split(/:/)[1].to_f
874         s << sprintf( "%d:%02d", t / 60, t % 60 )
876       else
877         s << x.to_s
879       end
880     end
881     return s
882   end
884   # Swap two songs in the playlist, either based on playlist indexes or song IDs (when <i>from_id</i> is true).
885   #
886   def swap(song_from, song_to, from_id = false)
887     if @@re['DIGITS_ONLY'].match(song_from.to_s) && @@re['DIGITS_ONLY'].match(song_to.to_s) then
888       return socket_puts("#{from_id ? 'swapid' : 'swap'} #{song_from} #{song_to}")
889     else 
890       raise "invalid input for swap"
891     end
892   end
893   
894   # Alias for MPD#swap(song_id_from, song_id_to, true)
895   #
896   def swap_id(song_id_from, song_id_to)
897     swap(song_id_from, song_id_to, true)
898   end
900   # Searches MP3 directory for new music and removes old music from the MPD database.
901   # <i>path</i> is an optional argument that specifies a particular directory or 
902   # song/file to update. <i>path</i> can also be a list of paths to update.
903   # If <i>path</i> is omitted, the entire database will be updated using the server's 
904   # base MP3 directory.
905   #
906   def update(path = '')
907     ulist = expand_list(path).flatten.uniq
908     if ulist.length == 1 then
909       return socket_puts("update #{ulist[0]}")
910     else
911       command_list_begin
912       ulist.each do |x|
913         command("update #{x}")
914       end
915       return command_list_end
916     end
917   end
918   
919   # Returns the types of URLs that can be handled by the server.
920   #
921   def urlhandlers
922     handlers = []
923     socket_puts("urlhandlers").each do |f|
924       handlers << f if /^handler: (.+)$/.match(f)
925     end
926     return handlers
927   end
929   # <b>Deprecated</b> Use MPD#setvol instead.
930   # Increase or decrease volume (depending on whether <i>vol_change</i> is positive or
931   # negative. Volume is limited to the range of 0-100 (server ensures that change
932   # does not take volume out of range).
933   # Returns volume.
934   #
935   def volume(vol_change)
936     warn "MPD#volume is deprecated. Use MPD#setvol instead."
937     socket_puts("volume #{vol_change}")
938     status['volume']
939   end
941   private :command, :command_list_begin, :command_list_end, :expand_list
942   private :connect, :get_server_response, :socket_puts
943   private :hash_to_songinfo, :response_to_songinfo