lib/mpd_client.rb in mpd_client-0.0.6 vs lib/mpd_client.rb in mpd_client-0.1.0
- old
+ new
@@ -1,423 +1,466 @@
+# frozen_string_literal: true
+
require 'socket'
-require "mpd_client/version"
+require 'mpd_client/version'
-HELLO_PREFIX = "OK MPD "
-ERROR_PREFIX = "ACK "
-SUCCESS = "OK"
-NEXT = "list_OK"
+module MPD
+ 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_object",
- # 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"
-}
+ # 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_object',
+ # 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'
+ }.freeze
-# 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
+ # The `MPD::Client` is used for interactions with a MPD server.
+ #
+ # Example:
+ #
+ # ```ruby
+ # require 'mpd_client'
+ # require 'logger'
+ #
+ # client = MPD::Client.new
+ # client.log = Logger.new($stderr)
+ # client.connect('/var/run/mpd/socket')
+ # ```
+ class Client
+ attr_reader :mpd_version
- class << self
- # Default logger for all MPDClient instances
- #
- # MPDClient.log = Logger.new($stderr)
- #
- attr_accessor :log
+ class << self
+ # Default logger for all `MPD::Client`` instances
+ #
+ # ```ruby
+ # MPD::Client.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)
+ def connect(host = 'localhost', port = 6600)
+ client = MPD::Client.new
+ client.connect(host, port)
+
+ client
end
+
+ def add_command(name, retval)
+ escaped_name = name.tr(' ', '_')
+ define_method escaped_name.to_sym do |*args|
+ ensure_connected
+
+ 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 remove_command(name)
- raise "Can't remove not existent '#{name}' command" unless method_defined? name.to_sym
- remove_method name.to_sym
+ def initialize
+ @mutex = Mutex.new
+ reset
end
- end
- def initialize
- @mutex = Mutex.new
- reset
- end
+ def connect(host = 'localhost', port = 6600)
+ @host = host
+ @port = port
- def connect(host = 'localhost', port = 6600)
- @host = host
- @port = port
- reconnect
- end
+ reconnect
+ end
- def reconnect
- log.info("MPD (re)connect #{@host}, #{@port}") if log
- if @host.start_with?('/')
- @socket = UNIXSocket.new(@host)
+ def reconnect
+ log&.info("MPD (re)connect #{@host}, #{@port}")
+
+ @socket = if @host.start_with?('/')
+ UNIXSocket.new(@host)
+ else
+ TCPSocket.new(@host, @port)
+ end
+
hello
- else
- @socket = TCPSocket.new(@host, @port)
- hello
+ @connected = true
end
- end
- def disconnect
- log.info("MPD disconnect") if log
- @socket.close
- reset
- end
+ def disconnect
+ log&.info('MPD disconnect')
+ @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 reset
+ @mpd_version = nil
+ @command_list = nil
+ @socket = nil
+ @log = nil
+ @connected = false
+ end
- def command_list_end
- raise "Not in command list" if @command_list.nil?
- write_command('command_list_end')
+ def connected?
+ @connected
+ end
- return fetch_command_list
- 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
- # The current logger. If no logger has been set MPDClient.log is used
- #
- def log
- @log || MPDClient.log
- end
+ def command_list_end
+ raise 'Not in command list' if @command_list.nil?
+ write_command('command_list_end')
- # Sets the +logger+ used by this instance of MPDClient
- #
- def log= logger
- @log = logger
- end
+ fetch_command_list
+ end
- private
+ # The current logger. If no logger has been set MPD::Client.log is used
+ def log
+ @log || MPD::Client.log
+ end
- def execute(command, *args, retval)
- @mutex.synchronize do
- if !@command_list.nil?
+ # Sets the +logger+ used by this instance of MPD::Client
+ attr_writer :log
+
+ private
+
+ def ensure_connected
+ raise 'Please connect to MPD server' unless connected?
+ end
+
+ def execute(command, *args, retval)
+ @mutex.synchronize do
write_command(command, *args)
- @command_list << retval
- else
- write_command(command, *args)
- eval retval
+
+ if !@command_list.nil?
+ @command_list << retval
+ else
+ eval retval
+ end
end
end
- end
- def write_line(line)
- begin
- @socket.puts line
- rescue Errno::EPIPE
- reconnect
- @socket.puts line
+ def write_line(line)
+ begin
+ @socket.puts line
+ rescue Errno::EPIPE
+ reconnect
+ @socket.puts line
+ end
+ @socket.flush
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)}\""
+ def write_command(command, *args)
+ parts = [command]
+ args.each do |arg|
+ line = if arg.is_a?(Array)
+ arg.size == 1 ? "\"#{arg[0].to_i}:\"" : "\"#{arg[0].to_i}:#{arg[1].to_i}\""
+ else
+ "\"#{escape(arg)}\""
+ end
+
+ parts << line
end
+ # log.debug("Calling MPD: #{command}#{args}") if log
+ log&.debug("Calling MPD: #{parts.join(' ')}")
+ write_line(parts.join(' '))
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
+ 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
+ if !@command_list.nil?
+ return if line == NEXT
+ raise "Got unexpected '#{SUCCESS}'" if line == SUCCESS
+ elsif line == SUCCESS
+ return
+ end
+
+ line
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
- 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
+ pair # Array
+ end
- return pair #Array
- end
-
- def read_pairs(separator = ': ')
- result = []
- pair = read_pair(separator)
- while pair
- result << pair
+ def read_pairs(separator = ': ')
+ result = []
pair = read_pair(separator)
+ while pair
+ result << pair
+ pair = read_pair(separator)
+ end
+
+ result
end
- return result
- end
+ def fetch_item
+ pairs = read_pairs
+ return nil if pairs.size != 1
- def fetch_item
- pairs = read_pairs
- return nil if pairs.size != 1
- return pairs[0][1]
- end
+ pairs[0][1]
+ end
- def fetch_nothing
- line = read_line
- raise "Got unexpected return value: #{line}" unless line.nil?
- end
+ def fetch_nothing
+ line = read_line
+ raise "Got unexpected 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}'"
+ def fetch_list
+ result = []
+ seen = nil
+
+ read_pairs.each do |key, value|
+ if key != seen
+ raise "Expected key '#{seen}', got '#{key}'" unless seen.nil?
+ seen = key
end
- seen = key
+
+ result << value
end
- result << value
+
+ result
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
+ 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
- obj[key] = value
- end
- result << obj unless obj.empty?
+ result << obj unless obj.empty?
- return result
- end
+ result
+ end
- def fetch_object
- objs = fetch_objects
- return objs ? objs[0] : {}
- end
+ def fetch_object
+ objs = fetch_objects
- def fetch_changes; fetch_objects(['cpos']); end
+ objs ? objs[0] : {}
+ end
- def fetch_songs; fetch_objects(['file']); end
+ def fetch_changes
+ fetch_objects(['cpos'])
+ end
- def fetch_mounts; fetch_objects(['mount']); end
+ def fetch_songs
+ fetch_objects(['file'])
+ end
- def fetch_neighbors; fetch_objects(['neighbor']); end
+ def fetch_mounts
+ fetch_objects(['mount'])
+ end
- def fetch_messages; fetch_objects('channel'); end
+ def fetch_neighbors
+ fetch_objects(['neighbor'])
+ end
- def fetch_outputs; fetch_objects(['outputid']); end
+ def fetch_messages
+ fetch_objects('channel')
+ end
- def fetch_plugins; fetch_objects(['plugin']); end
+ def fetch_outputs
+ fetch_objects(['outputid'])
+ end
- def fetch_database; fetch_objects(['file', 'directory', 'playlist']); end
+ def fetch_plugins
+ fetch_objects(['plugin'])
+ end
- def fetch_playlists; fetch_objects(['playlist']); end
+ def fetch_database
+ fetch_objects(%w[file directory playlist])
+ end
- def fetch_playlist
- result = []
- read_pairs(':').each do |key, value|
- result << value
+ def fetch_playlists
+ fetch_objects(['playlist'])
end
- return result
- end
+ def fetch_playlist
+ result = []
+ read_pairs(':').each do |_key, value|
+ result << value
+ 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]
+ result
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
- def fetch_sticker; fetch_stickers[0]; end
+ result
+ end
- def fetch_command_list
- result = []
- begin
- @command_list.each do |retval|
- result << (eval retval)
- end
- ensure
- @command_list = nil
+ def fetch_sticker
+ fetch_stickers[0]
end
- return result
- end
+ def fetch_command_list
+ result = []
+ begin
+ @command_list.each do |retval|
+ result << (eval retval)
+ end
+ ensure
+ @command_list = nil
+ end
+ 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 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
+ def escape(text)
+ text.to_s.gsub('\\', '\\\\').gsub('"', '\\"')
+ end
end
-
- def escape(text)
- text.to_s.gsub("\\", "\\\\").gsub('"', '\\"')
- end
-
end
-COMMANDS.each_pair do |name, callback|
- MPDClient.add_command(name, callback)
+MPD::COMMANDS.each_pair do |name, callback|
+ MPD::Client.add_command(name, callback)
end
-