Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1253 lines (1112 sloc) 36.4 KB
#
#== librmpd.rb
#
# librmpd.rb is another Ruby MPD Library with a goal of greater
# ease of use, more functionality, and thread safety
#
# Author:: Andrew Rader (bitwise_mcgee AT yahoo.com | http://nymb.us)
# Copyright:: Copyright (c) 2006 Andrew Rader
# License:: Distributed under the GNU GPL v2 (See COPYING file)
# Updates since 2009 by Jeffrey Aylesworth <jeffrey@aylesworth.ca>
#
# This was written with MPD version 0.11.5 (http://www.musicpd.org)
#
# The main class is the MPD class. This provides the functionality for
# talking to the server as well as setting up callbacks for when events
# occur (such as song changes, state changes, etc). The use of callbacks
# is optional, if they are used a seperate thread will continuously poll
# the server on its status, when something is changed, your program will
# be notified via any callbacks you have set. Most methods are the same
# as specified in the MPD Server Protocol, however some have been modified
# or renamed. Most notable is the list* and lsinfo functions have been
# replace with more sane methods (such as `files` for all files)
#
# http://github.com/jeffayle/librmpd2
#
#== Usage
#
# First create an MPD object
#
# require 'rubygems'
# require 'librmpd'
#
# mpd = MPD.new 'localhost', 6600
#
# and connect it to the server
#
# mpd.connect
#
# You can now issue any of the commands. Each command is documented below.
#
#=== Callbacks
#
# Callbacks are a way to easily setup your client as event based, rather
# than polling based. This means rather than having to check for changes
# in the server, you setup a few methods that will be called when those
# changes occur. For example, you could have a 'state_changed' method
# that will be called whenever the server changes state. You could then
# have this method change a label to reflect to the new state.
#
# To use callbacks in your program, first setup your callback methods. For
# example, say you have the class MyClient. Simply define whatever
# callbacks you want inside your class. See the documentation on the
# callback type constants in the MPD class for details on how each callback
# is called
#
# Once you have your callback methods defined, use the register_callback
# methods to inform librmpd about them. You can have multiple callbacks
# for each type of callback without problems. Simply use object.method('method_name')
# to get a reference to a Method object. Pass this object to the
# register_callback (along with the proper type value), and you're set.
#
# An Example:
#
# class MyClient
# ...
# def state_callback( newstate )
# puts "MPD Changed State: #{newstate}"
# end
# ...
# end
#
# client = MyClient.new
# mpd = MPD.new
# mpd.register_callback(client.method('state_callback'), MPD::STATE_CALLBACK)
#
# # Connect and Enable Callbacks
# mpd.connect( true )
#
# In order for the callback to be used, you must enable callbacks when you
# connect by passing true to the connect method. Now, whenever the state changes
# on the server, myclientobj's state_callback method will be called (and passed
# the new state as an argument)
class MPD
require 'socket'
require 'thread'
#
# These are the callback types used in registering callbacks
# STATE_CALLBACK: This is used to listen for changes in the server state
#
# The callback will always be called with a single string argument
# which may an empty string.
STATE_CALLBACK = 0
# CURRENT_SONG_CALLBACK: This is used to listen for changes in the current
#
# song being played by the server.
#
# The callback will always be called with a single argument, an MPD::Song
# object, or, if there were problems, nil
CURRENT_SONG_CALLBACK = 1
# PLAYLIST_CALLBACK: This is used to listen for when changes in the playlist
# are made.
#
# The callback will always be called with a single argument, an integer
# value for the current playlist or 0 if there were problems
PLAYLIST_CALLBACK = 2
# TIME_CALLBACK: This is used to listen for when the playback time changes
#
# The callback will always be called with two arguments. The first is
# the integer number of seconds elapsed (or 0 if errors), the second is
# the total number of seconds in the song (or 0 if errors)
TIME_CALLBACK = 3
# VOLUME_CALLBACK: This is used to listen for when the volume changes
#
# The callback will always be called with a single argument, an integer
# value of the volume (or 0 on errors)
VOLUME_CALLBACK = 4
# REPEAT_CALLBACK: This is used to listen for changes to the repeat flag
#
# The callback will always be called with a single argument, a boolean
# true or false depending on if the repeat flag is set / unset
REPEAT_CALLBACK = 5
# RANDOM_CALLBACK: This is used to listen for changed to the random flag
#
# The callback will always be called with a single argument, a boolean
# true or false depending on if the random flag is set / unset
RANDOM_CALLBACK = 6
# PLAYLIST_LENGTH_CALLBACK: This is used to listen for changes to the
# playlist length
#
# The callback will always be called with a single argument, an integer
# value of the current playlist's length (or 0 on errors)
PLAYLIST_LENGTH_CALLBACK = 7
# CROSSFADE_CALLBACK: This is used to listen for changes in the crossfade
# setting
#
# The callback will always be called with a single argument, an integer
# value of the number of seconds the crossfade is set to (or 0 on errsors)
CROSSFADE_CALLBACK = 8
# CURRENT_SONGID_CALLBACK: This is used to listen for changes in the
# current song's songid
#
# The callback will always be called with a single argument, an integer
# value of the id of the current song (or 0 on errors)
CURRENT_SONGID_CALLBACK = 9
# BITRATE_CALLBACK: This is used to listen for changes in the playback
# bitrate
#
# The callback will always be called with a single argument, an integer
# value of the bitrate of the playback (or 0 on errors)
BITRATE_CALLBACK = 10
# AUDIO_CALLBACK: This is used to listen for changes in the audio
# quality data (sample rate etc)
#
# The callback will always be called with three arguments, first,
# an integer holding the sample rate (or 0 on errors), next an
# integer holding the number of bits (or 0 on errors), finally an
# integer holding the number of channels (or 0 on errors)
AUDIO_CALLBACK = 11
# CONNECTION_CALLBACK: This is used to listen for changes in the
# connection to the server
#
# The callback will always be called with a single argument,
# a boolean true if the client is now connected to the server,
# and a boolean false if it has been disconnected
CONNECTION_CALLBACK = 12
#
#== Song
#
# This class is a glorified Hash used to represent a song
# You can access the various fields of a song (such as title) by
# either the normal hash method (song['title']) or by using
# the field as a method name (song.title).
#
# If the field doesn't exist or isn't set, nil will be returned
#
class Song < Hash
def method_missing(m, *a)
key = m.to_s
if key =~ /=$/
self[$`] = a[0]
elsif a.empty?
self[key]
else
raise NoMethodError, "#{m}"
end
end
end
# Initialize an MPD object with the specified hostname and port
# When called without arguments, 'localhost' and 6600 are used
def initialize( hostname = 'localhost', port = 6600 )
@hostname = hostname
@port = port
@socket = nil
@stop_cb_thread = false
@mutex = Mutex.new
@cb_thread = nil
@callbacks = []
@callbacks[STATE_CALLBACK] = []
@callbacks[CURRENT_SONG_CALLBACK] = []
@callbacks[PLAYLIST_CALLBACK] = []
@callbacks[TIME_CALLBACK] = []
@callbacks[VOLUME_CALLBACK] = []
@callbacks[REPEAT_CALLBACK] = []
@callbacks[RANDOM_CALLBACK] = []
@callbacks[PLAYLIST_LENGTH_CALLBACK] = []
@callbacks[CROSSFADE_CALLBACK] = []
@callbacks[CURRENT_SONGID_CALLBACK] = []
@callbacks[BITRATE_CALLBACK] = []
@callbacks[AUDIO_CALLBACK] = []
@callbacks[CONNECTION_CALLBACK] = []
end
# This will store the given method onto the given type's callback
# list. First you must get a reference to the method to call by
# the following:
#
# callback_method = my_object.method 'method name'
#
# Then you can call register_callback:
#
# mpd.register_callback( callback_method, MPD::STATE_CALLBACK )
#
# Now my_object's 'method name' method will be called whenever the
# state changes
def register_callback( method, type )
@callbacks[type].push method
end
#
# Connect to the daemon
# When called without any arguments, this will just
# connect to the server and wait for your commands
# When called with true as an argument, this will
# enable callbacks by starting a seperate polling thread.
# This polling thread will also automatically reconnect
# If is disconnected for whatever reason.
#
# connect will return OK plus the version string
# if successful, otherwise an error will be raised
#
# If connect is called on an already connected instance,
# a RuntimeError is raised
def connect( callbacks = false )
if self.connected?
raise 'MPD Error: Already Connected'
end
@socket = TCPSocket::new @hostname, @port
ret = @socket.gets # Read the version
if callbacks and (@cb_thread.nil? or !@cb_thread.alive?)
@stop_cb_thread = false
@cb_thread = Thread.new( self ) { |mpd|
old_status = {}
song = ''
connected = ''
while !@stop_cb_thread
begin
status = mpd.status
rescue
status = {}
end
begin
c = mpd.connected?
rescue
c = false
end
if connected != c
connected = c
for cb in @callbacks[CONNECTION_CALLBACK]
cb.call connected
end
end
if old_status['time'] != status['time']
if old_status['time'].nil? or old_status['time'].empty?
old_status['time'] = '0:0'
end
t = old_status['time'].split ':'
elapsed = t[0].to_i
total = t[1].to_i
for cb in @callbacks[TIME_CALLBACK]
cb.call elapsed, total
end
end
if old_status['volume'] != status['volume']
for cb in @callbacks[VOLUME_CALLBACK]
cb.call status['volume'].to_i
end
end
if old_status['repeat'] != status['repeat']
for cb in @callbacks[REPEAT_CALLBACK]
cb.call(status['repeat'] == '1')
end
end
if old_status['random'] != status['random']
for cb in @callbacks[RANDOM_CALLBACK]
cb.call(status['random'] == '1')
end
end
if old_status['playlist'] != status['playlist']
for cb in @callbacks[PLAYLIST_CALLBACK]
cb.call status['playlist'].to_i
end
end
if old_status['playlistlength'] != status['playlistlength']
for cb in @callbacks[PLAYLIST_LENGTH_CALLBACK]
cb.call status['playlistlength'].to_i
end
end
if old_status['xfade'] != status['xfade']
for cb in @callbacks[CROSSFADE_CALLBACK]
cb.call status['xfade'].to_i
end
end
if old_status['state'] != status['state']
state = (status['state'].nil? ? '' : status['state'])
for cb in @callbacks[STATE_CALLBACK]
cb.call state
end
end
begin
s = mpd.current_song
rescue
s = nil
end
if song != s
song = s
for cb in @callbacks[CURRENT_SONG_CALLBACK]
cb.call song
end
end
if old_status['songid'] != status['songid']
for cb in @callbacks[CURRENT_SONGID_CALLBACK]
cb.call status['songid'].to_i
end
end
if old_status['bitrate'] != status['bitrate']
for cb in @callbacks[BITRATE_CALLBACK]
cb.call status['bitrate'].to_i
end
end
if old_status['audio'] != status['audio']
audio = (status['audio'].nil? ? '0:0:0' : status['audio'])
a = audio.split ':'
samp = a[0].to_i
bits = a[1].to_i
chans = a[2].to_i
for cb in @callbacks[AUDIO_CALLBACK]
cb.call samp, bits, chans
end
end
old_status = status
sleep 0.1
if !connected
sleep 2
begin
mpd.connect unless @stop_cb_thread
rescue
end
end
end
}
end
return ret
end
#
# Check if the client is connected
#
# This will return true only if the server responds
# otherwise false is returned
def connected?
return false if @socket.nil?
begin
ret = send_command 'ping'
rescue
ret = false
end
return ret
end
#
# Disconnect from the server. This has no effect
# if the client is not connected. Reconnect using
# the connect method. This will also stop the
# callback thread, thus disabling callbacks
def disconnect
@stop_cb_thread = true
return if @socket.nil?
@socket.puts 'close'
@socket.close
@socket = nil
end
#
# Add the file _path_ to the playlist. If path is a
# directory, it will be added recursively.
#
# Returns true if this was successful,
# Raises a RuntimeError if the command failed
def add( path )
send_command "add \"#{escape path}\""
end
#
# Clears the current playlist
#
# Returns true if this was successful,
# Raises a RuntimeError if the command failed
def clear
send_command 'clear'
end
#
# Clears the current error message reported in status
# ( This is also accomplished by any command that starts playback )
#
# Returns true if this was successful,
# Raises a RuntimeError if the command failed
def clearerror
send_command 'clearerror'
end
#
# Set the crossfade between songs in seconds
#
# Raises a RuntimeError if the command failed
def crossfade=( seconds )
send_command "crossfade #{seconds}"
end
#
# Read the crossfade between songs in seconds,
# Raises a RuntimeError if the command failed
def crossfade
status = self.status
return if status.nil?
return status['xfade'].to_i
end
#
# Read the currently playing song
#
# Returns a Song object with the current song's data,
# Raises a RuntimeError if the command failed
def current_song
build_song( send_command('currentsong') )
end
#
# Delete the song from the playlist, where pos
# is the song's position in the playlist
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def delete( pos )
send_command "delete #{pos}"
end
#
# Delete the song with the songid from the playlist
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def deleteid( songid )
send_command "deleteid #{songid}"
end
#
# Finds songs in the database that are EXACTLY
# matched by the what argument. type should be
# 'album', 'artist', or 'title'
#
# This returns an Array of MPD::Songs,
# Raises a RuntimeError if the command failed
def find( type, what )
response = send_command "find \"#{type}\" \"#{escape what}\""
build_songs_list response
end
#
# Kills MPD
#
# Returns true if successful.
# Raises a RuntimeError if the command failed
def kill
send_command 'kill'
end
#
# Lists all of the albums in the database
# The optional argument is for specifying an
# artist to list the albums for
#
# Returns an Array of Album names (Strings),
# Raises a RuntimeError if the command failed
def albums( artist = nil )
list 'album', artist
end
#
# Lists all of the artists in the database
#
# Returns an Array of Artist names (Strings),
# Raises a RuntimeError if the command failed
def artists
list 'artist'
end
#
# This is used by the albums and artists methods
# type should be 'album' or 'artist'. If type is 'album'
# then arg can be a specific artist to list the albums for
#
# Returns an Array of Strings,
# Raises a RuntimeError if the command failed
def list( type, arg = nil )
if not arg.nil?
response = send_command "list #{type} \"#{escape arg}\""
else
response = send_command "list #{type}"
end
list = []
if not response.nil? and response.kind_of? String
lines = response.split "\n"
re = Regexp.new "\\A#{type}: ", 'i'
for line in lines
list << line.gsub( re, '' )
end
end
return list
end
#
# List all of the directories in the database, starting at path.
# If path isn't specified, the root of the database is used
#
# Returns an Array of directory names (Strings),
# Raises a RuntimeError if the command failed
#
# If recursive is true (default), returns all subdirectories,
# or else returns only direct children
def directories( path = nil, recursive=true )
response = ''
if recursive
if not path.nil?
response = send_command "listall \"#{escape path}\""
else
response = send_command 'listall'
end
else #Non recursive directory listing
response = send_command("lsinfo \"#{escape(path) || '/'}\"")
end
filter_response response, /\Adirectory: /i
end
#
# List all of the files in the database, starting at path.
# If path isn't specified, the root of the database is used
#
# Returns an Array of file names (Strings).
# Raises a RuntimeError if the command failed
#
# If recursive is true (default), returns all subdirectories,
# or else returns only direct children
def files( path = nil, recursive=true )
response = ''
if recursive
if not path.nil?
response = send_command "listall \"#{escape path}\""
else
response = send_command 'listall'
end
else
response = send_command("lsinfo \"#{escape(path) || '/'}\"")
end
filter_response response, /\Afile: /i
end
#
# List all of the playlists in the database
#
# Returns an Array of playlist names (Strings)
def playlists
response = send_command 'lsinfo'
filter_response response, /\Aplaylist: /i
end
#
# List all of the songs in the database starting at path.
# If path isn't specified, the root of the database is used
#
# Returns an Array of MPD::Songs,
# Raises a RuntimeError if the command failed
#
# If recursive is true (default), returns all subdirectories,
# or else returns only direct children
def songs( path = nil, recursive=true )
response = ''
if recursive
if not path.nil?
response = send_command "listallinfo \"#{escape path}\""
else
response = send_command 'listallinfo'
end
else #Non recursive
response = send_command "lsinfo \"#{escape(path) || '/'}\""
end
build_songs_list response
end
#
# List all of the songs by an artist
#
# Returns an Array of MPD::Songs by the artist `artist`,
# Raises a RuntimeError if the command failed
def songs_by_artist( artist )
all_songs = self.songs
artist_songs = []
all_songs.each do |song|
if song.artist == artist
artist_songs << song
end
end
return artist_songs
end
#
# Loads the playlist name.m3u (do not pass the m3u extension
# when calling) from the playlist directory. Use `playlists`
# to what playlists are available
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def load( name )
send_command "load \"#{name}\""
end
#
# Move the song at `from` to `to` in the playlist
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def move( from, to )
send_command "move #{from} #{to}"
end
#
# Move the song with the `songid` to `to` in the playlist
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def moveid( songid, to )
send_command "moveid #{songid} #{to}"
end
#
# Plays the next song in the playlist
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def next
send_command 'next'
end
#
# Set / Unset paused playback
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def pause=( toggle )
send_command 'pause ' + (toggle ? '1' : '0')
end
#
# Returns true if MPD is paused,
# Raises a RuntimeError if the command failed
def paused?
status = self.status
return false if status.nil?
return status['state'] == 'pause'
end
#
# This is used for authentication with the server
# `pass` is simply the plaintext password
#
# Raises a RuntimeError if the command failed
def password( pass )
send_command "password \"#{pass}\""
end
#
# Ping the server
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def ping
send_command 'ping'
end
#
# Begin playing the playist. Optionally
# specify the pos to start on
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def play( pos = nil )
if pos.nil?
return send_command('play')
else
return send_command("play #{pos}")
end
end
#
# Returns true if the server's state is set to 'play',
# Raises a RuntimeError if the command failed
def playing?
state = self.status['state']
return state == 'play'
end
#
# Begin playing the playlist. Optionally
# specify the songid to start on
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def playid( songid = nil )
if not songid.nil?
return(send_command("playid #{songid}"))
else
return(send_command('playid'))
end
end
#
# Returns the current playlist version number,
# Raises a RuntimeError if the command failed
def playlist_version
self.status['playlist'].to_i
end
#
# List the current playlist
# This is the same as playlistinfo w/o args
#
# Returns an Array of MPD::Songs,
# Raises a RuntimeError if the command failed
def playlist
response = send_command 'playlistinfo'
build_songs_list response
end
#
# Returns the MPD::Song at the position `pos` in the playlist,
# Raises a RuntimeError if the command failed
def song_at_pos( pos )
build_song( send_command("playlistinfo #{pos}") )
end
#
# Returns the MPD::Song with the `songid` in the playlist,
# Raises a RuntimeError if the command failed
def song_with_id( songid )
build_song( send_command("playlistid #{songid}") )
end
#
# List the changes since the specified version in the playlist
#
# Returns an Array of MPD::Songs,
# Raises a RuntimeError if the command failed
def playlist_changes( version )
response = send_command "plchanges #{version}"
build_songs_list response
end
#
# Plays the previous song in the playlist
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def previous
send_command 'previous'
end
#
# Enable / Disable random playback,
# Raises a RuntimeError if the command failed
def random=( toggle )
send_command 'random ' + (toggle ? '1' : '0')
end
#
# Returns true if random playback is currently enabled,
# Raises a RuntimeError if the command failed
def random?
rand = self.status['random']
return rand == '1'
end
#
# Enable / Disable repeat,
# Raises a RuntimeError if the command failed
def repeat=( toggle )
send_command 'repeat ' + (toggle ? '1' : '0')
end
#
# Returns true if repeat is enabled,
# Raises a RuntimeError if the command failed
def repeat?
repeat = self.status['repeat']
return repeat == '1'
end
#
# Removes (PERMANENTLY!) the playlist `playlist.m3u` from
# the playlist directory
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def rm( playlist )
send_command "rm \"#{escape playlist}\""
end
#
# An Alias for rm
def remove_playlist( playlist )
rm playlist
end
#
# Saves the current playlist to `playlist`.m3u in the
# playlist directory
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def save( playlist )
send_command "save \"#{escape playlist}\""
end
#
# Searches for any song that contains `what` in the `type` field
# `type` can be 'title', 'artist', 'album' or 'filename'
# Searches are NOT case sensitive
#
# Returns an Array of MPD::Songs,
# Raises a RuntimeError if the command failed
def search( type, what )
build_songs_list( send_command("search #{type} \"#{escape what}\"") )
end
#
# Seeks to the position `time` (in seconds) of the
# song at `pos` in the playlist
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def seek( pos, time )
send_command "seek #{pos} #{time}"
end
#
# Seeks to the position `time` (in seconds) of the song with
# the id `songid`
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def seekid( songid, time )
send_command "seekid #{songid} #{time}"
end
#
# Set the volume
# The argument `vol` will automatically be bounded to 0 - 100
#
# Raises a RuntimeError if the command failed
def volume=( vol )
send_command "setvol #{vol}"
end
#
# Returns the volume,
# Raises a RuntimeError if the command failed
def volume
status = self.status
return if status.nil?
return status['volume'].to_i
end
#
# Shuffles the playlist,
# Raises a RuntimeError if the command failed
def shuffle
send_command 'shuffle'
end
#
# Returns a Hash of MPD's stats,
# Raises a RuntimeError if the command failed
def stats
response = send_command 'stats'
build_hash response
end
#
# Returns a Hash of the current status,
# Raises a RuntimeError if the command failed
def status
response = send_command 'status'
build_hash response
end
#
# Stop playing
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def stop
send_command 'stop'
end
#
# Returns true if the server's state is 'stop',
# Raises a RuntimeError if the command failed
def stopped?
status = self.status
return false if status.nil?
return status['state'] == 'stop'
end
#
# Swaps the song at position `posA` with the song
# as position `posB` in the playlist
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def swap( posA, posB )
send_command "swap #{posA} #{posB}"
end
#
# Swaps the song with the id `songidA` with the song
# with the id `songidB`
#
# Returns true if successful,
# Raises a RuntimeError if the command failed
def swapid( songidA, songidB )
send_command "swapid #{songidA} #{songidB}"
end
#
# Tell the server to update the database. Optionally,
# specify the path to update
def update( path = nil )
ret = ''
if not path.nil?
ret = send_command("update \"#{escape path}\"")
else
ret = send_command('update')
end
return(ret.gsub('updating_db: ', '').to_i)
end
#
# Gives a list of all outputs
def outputs
build_outputs_list(send_command("outputs"))
end
#
# Enables output num
def enableoutput(num)
send_command("enableoutput #{num.to_s}")
end
#
# Disables output num
def disableoutput(num)
send_command("disableoutput #{num.to_s}")
end
#
# Private Method
#
# Used to send a command to the server. This synchronizes
# on a mutex to be thread safe
#
# Returns the server response as processed by `handle_server_response`,
# Raises a RuntimeError if the command failed
def send_command( command )
if @socket.nil?
raise "MPD: Not Connected to the Server"
end
ret = nil
@mutex.synchronize do
begin
@socket.puts command
ret = handle_server_response
rescue Errno::EPIPE
@socket = nil
raise 'MPD Error: Broken Pipe (Disconnected)'
end
end
return ret
end
#
# Gets the time into and total time of the currently play song
# as an array of [time into, total time]
def time()
t = self.status['time']
if t
t = t.split ':'
[t[0].to_i, t[1].to_i]
else
[0, 1] #When not playing anything.
#Second index is not zero to avoid /0
end
end
#
# Private Method
#
# Handles the server's response (called inside send_command)
#
# This will repeatedly read the server's response from the socket
# and will process the output. If a string is returned by the server
# that is what is returned. If just an "OK" is returned, this returns
# true. If an "ACK" is returned, this raises an error
def handle_server_response
return if @socket.nil?
msg = ''
reading = true
error = nil
while reading
line = @socket.gets
case line
when "OK\n"
reading = false
when /^ACK/
error = line
reading = false
when nil
reading = false
else
msg += line
end
end
if error.nil?
return true if msg.empty?
return msg
else
raise error.gsub( /^ACK \[(\d+)\@(\d+)\] \{(.+)\} (.+)$/, 'MPD Error #\1: \3: \4')
end
end
#
# Private Method
#
# This builds a hash out of lines returned from the server.
# First the response is turned into an array of lines
# then each entry is parsed so that the line is viewed as
# "key: value"
#
# The end result is a hash containing the proper key/value pairs
def build_hash( string )
return {} if string.nil? or !string.kind_of? String
hash = {}
lines = string.split "\n"
lines.each do |line|
hash[ line.gsub(/:.*/, '').downcase ] = line.gsub(/\A[^:]*: /, '')
end
return hash
end
#
# Private Method
#
# This is similar to build_hash, but instead of building a Hash,
# a MPD::Song is built
def build_song( string )
return if string.nil? or !string.kind_of? String
song = Song.new
lines = string.split "\n"
lines.each do |line|
song[ line.gsub(/:.*/, '').downcase ] = line.gsub(/\A[^:]*: /, '')
end
return song
end
#
# Private Method
#
# This first creates an array of lines as returned from the server
# Then each entry is processed and added to an MPD::Song
# Whenever a new 'file:' entry is found, the current MPD::Song
# is added to an array, and a new one is created
#
# The end result is an Array of MPD::Songs
def build_songs_list( string )
return [] if string.nil? or !string.kind_of? String
list = []
song = Song.new
lines = string.split "\n"
lines.each do |line|
key = line.gsub(/:.*/, '')
line.gsub!(/\A[^:]*: /, '')
if key == 'file' && !song.file.nil?
list << song
song = Song.new
end
song[key.downcase] = line
end
list << song
#For some reason this is picking up directories some times, take
#them out
return list.select{|n| not n['directory'] }
end
#
# Private Method
#
# This first creates an array of lines as returned from the server
# Then each entry is processed and added to an Hash
# Whenever a new 'outputid:' entry is found, the current Hash
# is added to an array, and a new one is created
#
# The end result is an Array of Hashes(containing the outputs)
def build_outputs_list( string )
return [] if string.nil? or !string.kind_of? String
list = []
output = {}
lines = string.split "\n"
lines.each do |line|
key = line.gsub(/:.*/, '')
line.gsub!(/\A[^:]*: /, '')
if key == 'outputid' && !output['outputid'].nil?
list << output
end
output[key.downcase] = line
end
list << output
return list
end
#
# Private Method
#
# This filters each line from the server to return
# only those matching the regexp. The regexp is removed
# from the line before it is added to an Array
#
# This is used in the `directories`, `files`, etc methods
# to return only the directory/file names
def filter_response( string, regexp )
list = []
lines = string.split "\n"
lines.each do |line|
if line =~ regexp
list << line.gsub(regexp, '')
end
end
return list
end
#
# Private Method
#
# Escapes a string for MPD
def escape str
str.gsub /"/, '\"'
end
private :send_command
private :handle_server_response
private :build_hash
private :build_song
private :build_songs_list
private :build_outputs_list
private :filter_response
private :escape
end