Edit

thodg/mpd_client/lib/mpd_client.rb

Branch :

  • Show log

    Commit

  • Author : Anton Maminov
    Date : 2013-11-06 12:47:22
    Hash : b3b5e23e
    Message : add support for volume

  • lib/mpd_client.rb
  • # 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://mpd.wikia.com/wiki/MusicPlayerDaemonCommands
    # 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",
      "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",
      "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",
      "lsinfo"             => "fetch_database",
      "search"             => "fetch_songs",
      "searchadd"          => "fetch_nothing",
      "searchaddp1"        => "fetch_nothing",
      "update"             => "fetch_item",
      "rescan"             => "fetch_item",
      "readcomments"       => "fetch_item",
      # 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
        reset
      end
    
      def connect(host = 'localhost', port = 6600)
        log.info("MPD 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)
        if !@command_list.nil?
          write_command(command, *args)
          @command_list << retval
        else
          write_command(command, *args)
          eval retval
        end
      end
    
      def write_line(line)
        @socket.puts line
        @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_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