# encoding: utf-8 require 'socket' require "mpd_client/version" HELLO_PREFIX = "OK MPD " ERROR_PREFIX = "ACK " SUCCESS = "OK" NEXT = "list_OK" # MPD changelog: http://git.musicpd.org/cgit/master/mpd.git/plain/NEWS # http://www.musicpd.org/doc/protocol/command_reference.html # http://git.musicpd.org/cgit/cirrus/mpd.git/plain/doc/protocol.xml # COMMANDS = { # Status Commands "clearerror" => "fetch_nothing", "currentsong" => "fetch_object", "idle" => "fetch_list", "noidle" => "", "status" => "fetch_object", "stats" => "fetch_object", # Playback Option Commands "consume" => "fetch_nothing", "crossfade" => "fetch_nothing", "mixrampdb" => "fetch_nothing", "mixrampdelay" => "fetch_nothing", "random" => "fetch_nothing", "repeat" => "fetch_nothing", "setvol" => "fetch_nothing", "single" => "fetch_nothing", "replay_gain_mode" => "fetch_nothing", "replay_gain_status" => "fetch_item", "volume" => "fetch_nothing", # Playback Control Commands "next" => "fetch_nothing", "pause" => "fetch_nothing", "play" => "fetch_nothing", "playid" => "fetch_nothing", "previous" => "fetch_nothing", "seek" => "fetch_nothing", "seekid" => "fetch_nothing", "seekcur" => "fetch_nothing", "stop" => "fetch_nothing", # Playlist Commands "add" => "fetch_nothing", "addid" => "fetch_item", "addtagid" => "fetch_nothing", "cleartagid" => "fetch_nothing", "clear" => "fetch_nothing", "delete" => "fetch_nothing", "deleteid" => "fetch_nothing", "move" => "fetch_nothing", "moveid" => "fetch_nothing", "playlist" => "fetch_playlist", "playlistfind" => "fetch_songs", "playlistid" => "fetch_songs", "playlistinfo" => "fetch_songs", "playlistsearch" => "fetch_songs", "plchanges" => "fetch_songs", "plchangesposid" => "fetch_changes", "prio" => "fetch_nothing", "prioid" => "fetch_nothing", "rangeid" => "fetch_nothing", "shuffle" => "fetch_nothing", "swap" => "fetch_nothing", "swapid" => "fetch_nothing", # Stored Playlist Commands "listplaylist" => "fetch_list", "listplaylistinfo" => "fetch_songs", "listplaylists" => "fetch_playlists", "load" => "fetch_nothing", "playlistadd" => "fetch_nothing", "playlistclear" => "fetch_nothing", "playlistdelete" => "fetch_nothing", "playlistmove" => "fetch_nothing", "rename" => "fetch_nothing", "rm" => "fetch_nothing", "save" => "fetch_nothing", # Database Commands "count" => "fetch_object", "find" => "fetch_songs", "findadd" => "fetch_nothing", "list" => "fetch_list", "listall" => "fetch_database", "listallinfo" => "fetch_database", "listfiles" => "fetch_database", "lsinfo" => "fetch_database", "search" => "fetch_songs", "searchadd" => "fetch_nothing", "searchaddp1" => "fetch_nothing", "update" => "fetch_item", "rescan" => "fetch_item", "readcomments" => "fetch_item", # Mounts and neighbors "mount" => "fetch_nothing", "unmount" => "fetch_nothing", "listmounts" => "fetch_mounts", "listneighbors" => "fetch_neighbors", # Sticker Commands "sticker get" => "fetch_sticker", "sticker set" => "fetch_nothing", "sticker delete" => "fetch_nothing", "sticker list" => "fetch_stickers", "sticker find" => "fetch_songs", # Connection Commands "close" => "", "kill" => "", "password" => "fetch_nothing", "ping" => "fetch_nothing", # Audio Output Commands "disableoutput" => "fetch_nothing", "enableoutput" => "fetch_nothing", "outputs" => "fetch_outputs", "toggleoutput" => "fetch_nothing", # Reflection Commands "config" => "fetch_item", "commands" => "fetch_list", "notcommands" => "fetch_list", "tagtypes" => "fetch_list", "urlhandlers" => "fetch_list", "decoders" => "fetch_plugins", # Client To Client "subscribe" => "fetch_nothing", "unsubscribe" => "fetch_nothing", "channels" => "fetch_list", "readmessages" => "fetch_messages", "sendmessage" => "fetch_nothing" } # The MPDClient library is used for interactions with a MPD. # # == Example # # require 'mpd_client' # require 'logger' # # client = MPDClient.new # client.log = Logger.new($stderr) # client.connect('/var/run/mpd/socket') # class MPDClient attr_reader :mpd_version class << self # Default logger for all MPDClient instances # # MPDClient.log = Logger.new($stderr) # attr_accessor :log def add_command(name, retval) escaped_name = name.gsub(' ', '_') define_method escaped_name.to_sym do |*args| execute(name, *args, retval) end end def remove_command(name) raise "Can't remove not existent '#{name}' command" unless method_defined? name.to_sym remove_method name.to_sym end end def initialize @mutex = Mutex.new reset end def connect(host = 'localhost', port = 6600) @host = host @port = port reconnect end def reconnect log.info("MPD (re)connect #{@host}, #{@port}") if log if @host.start_with?('/') @socket = UNIXSocket.new(@host) hello else @socket = TCPSocket.new(@host, @port) hello end end def disconnect log.info("MPD disconnect") if log @socket.close reset end # http://www.musicpd.org/doc/protocol/ch01s04.html def command_list_ok_begin raise "Already in command list" unless @command_list.nil? write_command('command_list_ok_begin') @command_list = [] end def command_list_end raise "Not in command list" if @command_list.nil? write_command('command_list_end') return fetch_command_list end # The current logger. If no logger has been set MPDClient.log is used # def log @log || MPDClient.log end # Sets the +logger+ used by this instance of MPDClient # def log= logger @log = logger end private def execute(command, *args, retval) @mutex.synchronize do if !@command_list.nil? write_command(command, *args) @command_list << retval else write_command(command, *args) eval retval end end end def write_line(line) begin @socket.puts line rescue Errno::EPIPE reconnect @socket.puts line end @socket.flush end def write_command(command, *args) parts = [command] args.each do |arg| if arg.kind_of?(Array) parts << (arg.size == 1 ? "\"#{arg[0].to_i}:\"" : "\"#{arg[0].to_i}:#{arg[1].to_i}\"") else parts << "\"#{escape(arg)}\"" end end #log.debug("Calling MPD: #{command}#{args}") if log log.debug("Calling MPD: #{parts.join(' ')}") if log write_line(parts.join(' ')) end def read_line line = @socket.gets.force_encoding('utf-8') raise "Connection lost while reading line" unless line.end_with?("\n") line.chomp! if line.start_with?(ERROR_PREFIX) error = line[/#{ERROR_PREFIX}(.*)/, 1].strip raise error end if !@command_list.nil? return if line == NEXT raise "Got unexpected '#{SUCCESS}'" if line == SUCCESS elsif line == SUCCESS return end return line end def read_pair(separator) line = read_line return if line.nil? pair = line.split(separator, 2) raise "Could now parse pair: '#{line}'" if pair.size < 2 return pair #Array end def read_pairs(separator = ': ') result = [] pair = read_pair(separator) while pair result << pair pair = read_pair(separator) end return result end def fetch_item pairs = read_pairs return nil if pairs.size != 1 return pairs[0][1] end def fetch_nothing line = read_line raise "Got unexpected return value: #{line}" unless line.nil? end def fetch_list result = [] seen = nil read_pairs.each do |key, value| if key != seen if seen != nil raise "Expected key '#{seen}', got '#{key}'" end seen = key end result << value end return result end def fetch_objects(delimeters = []) result = [] obj = {} read_pairs.each do |key, value| key = key.downcase if delimeters.include?(key) result << obj unless obj.empty? obj = {} elsif obj.include?(key) obj[key] << value end obj[key] = value end result << obj unless obj.empty? return result end def fetch_object objs = fetch_objects return objs ? objs[0] : {} end def fetch_changes; fetch_objects(['cpos']); end def fetch_songs; fetch_objects(['file']); end def fetch_mounts; fetch_objects(['mount']); end def fetch_neighbors; fetch_objects(['neighbor']); end def fetch_messages; fetch_objects('channel'); end def fetch_outputs; fetch_objects(['outputid']); end def fetch_plugins; fetch_objects(['plugin']); end def fetch_database; fetch_objects(['file', 'directory', 'playlist']); end def fetch_playlists; fetch_objects(['playlist']); end def fetch_playlist result = [] read_pairs(':').each do |key, value| result << value end return result end def fetch_stickers result = [] read_pairs.each do |key, sticker| value = sticker.split('=', 2) raise "Could now parse sticker: #{sticker}" if value.size < 2 result << Hash[*value] end return result end def fetch_sticker; fetch_stickers[0]; end def fetch_command_list result = [] begin @command_list.each do |retval| result << (eval retval) end ensure @command_list = nil end return result end def hello line = @socket.gets raise "Connection lost while reading MPD hello" unless line.end_with?("\n") line.chomp! raise "Got invalid MPD hello: #{line}" unless line.start_with?(HELLO_PREFIX) @mpd_version = line[/#{HELLO_PREFIX}(.*)/, 1] end def reset @mpd_version = nil @command_list = nil @socket = nil @log = nil end def escape(text) text.to_s.gsub("\\", "\\\\").gsub('"', '\\"') end end COMMANDS.each_pair do |name, callback| MPDClient.add_command(name, callback) end