lib/mpd_client.rb in mpd_client-0.1.0 vs lib/mpd_client.rb in mpd_client-0.2.0
- old
+ new
@@ -1,133 +1,132 @@
# frozen_string_literal: true
require 'socket'
+require 'stringio'
require 'mpd_client/version'
module MPD
HELLO_PREFIX = 'OK MPD '
ERROR_PREFIX = 'ACK '
- SUCCESS = 'OK'
- NEXT = 'list_OK'
+ SUCCESS = "OK\n"
+ NEXT = "list_OK\n"
- # 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
+ # MPD changelog: https://github.com/MusicPlayerDaemon/MPD/blob/master/NEWS
+ # Protocol: https://mpd.readthedocs.io/en/latest/protocol.html
COMMANDS = {
# Status Commands
- 'clearerror' => 'fetch_nothing',
- 'currentsong' => 'fetch_object',
- 'idle' => 'fetch_list',
- 'noidle' => '',
- 'status' => 'fetch_object',
- 'stats' => 'fetch_object',
+ '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',
+ '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',
+ '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',
+ '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',
+ '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',
+ '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',
+ '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',
+ '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',
+ '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',
+ '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',
+ 'close' => '',
+ 'kill' => '',
+ 'password' => 'fetch_nothing',
+ 'ping' => 'fetch_nothing',
# Audio Output Commands
- 'disableoutput' => 'fetch_nothing',
- 'enableoutput' => 'fetch_nothing',
- 'outputs' => 'fetch_outputs',
- 'toggleoutput' => 'fetch_nothing',
+ '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',
+ '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'
+ 'subscribe' => 'fetch_nothing',
+ 'unsubscribe' => 'fetch_nothing',
+ 'channels' => 'fetch_list',
+ 'readmessages' => 'fetch_messages',
+ 'sendmessage' => 'fetch_nothing'
}.freeze
# The `MPD::Client` is used for interactions with a MPD server.
#
# Example:
@@ -158,19 +157,21 @@
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 initialize
@@ -186,15 +187,16 @@
end
def reconnect
log&.info("MPD (re)connect #{@host}, #{@port}")
- @socket = if @host.start_with?('/')
- UNIXSocket.new(@host)
- else
- TCPSocket.new(@host, @port)
- end
+ @socket =
+ if @host.start_with?('/')
+ UNIXSocket.new(@host)
+ else
+ TCPSocket.new(@host, @port)
+ end
hello
@connected = true
end
@@ -214,19 +216,22 @@
def connected?
@connected
end
- # http://www.musicpd.org/doc/protocol/ch01s04.html
+ # https://www.musicpd.org/doc/protocol/command_lists.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')
fetch_command_list
end
@@ -236,24 +241,32 @@
end
# Sets the +logger+ used by this instance of MPD::Client
attr_writer :log
+ def albumart(uri)
+ fetch_binary(StringIO.new, 0, 'albumart', uri)
+ end
+
+ def readpicture(uri)
+ fetch_binary(StringIO.new, 0, 'readpicture', uri)
+ end
+
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)
- if !@command_list.nil?
- @command_list << retval
- else
+ if @command_list.nil?
eval retval
+ else
+ @command_list << retval
end
end
end
def write_line(line)
@@ -261,33 +274,38 @@
@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|
- 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
+ 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
def read_line
- line = @socket.gets.force_encoding('utf-8')
+ line = @socket.gets
+
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
@@ -299,49 +317,55 @@
end
line
end
- def read_pair(separator)
+ def read_pair
line = read_line
+
return if line.nil?
- pair = line.split(separator, 2)
- raise "Could now parse pair: '#{line}'" if pair.size < 2
- pair # Array
+ line.split(': ', 2)
end
- def read_pairs(separator = ': ')
+ def read_pairs
result = []
- pair = read_pair(separator)
+
+ pair = read_pair
+
while pair
result << pair
- pair = read_pair(separator)
+ pair = read_pair
end
result
end
def fetch_item
pairs = read_pairs
+
return nil if pairs.size != 1
pairs[0][1]
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|
+ value = value.chomp.force_encoding('utf-8')
+
if key != seen
raise "Expected key '#{seen}', got '#{key}'" unless seen.nil?
+
seen = key
end
result << value
end
@@ -350,18 +374,22 @@
end
def fetch_objects(delimeters = [])
result = []
obj = {}
+
read_pairs.each do |key, value|
key = key.downcase
+ value = value.chomp.force_encoding('utf-8')
+
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?
@@ -372,10 +400,45 @@
objs = fetch_objects
objs ? objs[0] : {}
end
+ def fetch_binary(io = StringIO.new, offset = 0, *args)
+ data = {}
+
+ @mutex.synchronize do
+ write_command(*args, offset)
+
+ binary = false
+
+ read_pairs.each do |item|
+ if binary
+ io << item.join(': ')
+ next
+ end
+
+ key = item[0]
+ value = item[1].chomp
+
+ binary = (key == 'binary')
+
+ data[key] = value
+ end
+ end
+
+ size = data['size'].to_i
+ binary = data['binary'].to_i
+
+ next_offset = offset + binary
+
+ return [data, io] if next_offset >= size
+
+ io.seek(-1, IO::SEEK_CUR)
+
+ fetch_binary(io, next_offset, *args)
+ end
+
def fetch_changes
fetch_objects(['cpos'])
end
def fetch_songs
@@ -408,24 +471,17 @@
def fetch_playlists
fetch_objects(['playlist'])
end
- def fetch_playlist
- result = []
- read_pairs(':').each do |_key, value|
- result << value
- end
-
- 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
result
end
@@ -434,10 +490,11 @@
fetch_stickers[0]
end
def fetch_command_list
result = []
+
begin
@command_list.each do |retval|
result << (eval retval)
end
ensure
@@ -447,12 +504,16 @@
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 escape(text)
text.to_s.gsub('\\', '\\\\').gsub('"', '\\"')