Skip to content

mamantoha/crystal_mpd

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

MPD::Client

Crystal CI GitHub release Docs License

Concurrent Music Player Daemon client written entirely in Crystal.

Main features

  • Filtering DSL
  • Range support
  • Callbacks
  • Command lists support
  • Binary responses
  • Client to client communicate
  • Logging
  • Handle exceptions

Installation

Add this to your application's shard.yml:

dependencies:
  crystal_mpd:
    github: mamantoha/crystal_mpd

Usage

require "crystal_mpd"

All functionality is contained in the MPD::Client class. Creating an instance of this class is as simple as:

client = MPD::Client.new("localhost", 6600)

You can also omit the host and port, and it will use the defaults:

client = MPD::Client.new("localhost")
client = MPD::Client.new

You can connect to a local socket (UNIX domain socket), specify an absolute path:

client = MPD::Client.new("/run/mpd/socket")

If a password specified for access to MPD:

client = MPD::Client.new("localhost", password: "password")

The client library can be used as follows:

puts client.version # print the mpd version
client.play(2)      # begins playing the playlist at song number 2
puts client.status  # print the current status of the player and the volume level
client.close        # send the close command
client.disconect    # disconnect from the server

Check MPD::Client source for supported commands.

To use all crystal_mpd functions you should use the latest stable MPD version (0.24.x).

Command lists

Command lists documentation.

To facilitate faster adding of files etc. you can pass a list of commands all at once using a command list. The command list begins with command_list_ok_begin and ends with command_list_end.

It does not execute any commands until the list has ended. The return value is whatever the return for a list of commands is. On success for all commands, OK is returned.

If a command fails, no more commands are executed and the appropriate ACK error is returned.

If command_list_ok_begin is used, list_OK is returned for each successful command executed in the command list.

client.command_list_ok_begin # start a command list
client.update                # insert the update command into the list
client.status                # insert the status command into the list
client.command_list_end      # result will be a Array with the results

or

client.with_command_list do
  client.update
  client.status
end

Ranges

Ranges documentation.

Some MPD commands (e.g. move, delete, load, shuffle, playlistinfo) support integer ranges in the format START:END, specifying a slice of songs. This is handled in crystal_mpd via MPD::Range, which supports both inclusive (1..10) and exclusive (1...10) ranges.

Note: MPD treats END as exclusive, so we internally adjust inclusive ranges to match this behavior. Also note that in MPD, song indexes start at 0 β€” the same as in Crystal.

# Move first 3 songs to position 10, 11, and 12
client.move(0..2, 10)

# Delete songs 0 and 1 (but NOT 2)
client.delete(0...2)

# Delete songs 0, 1, and 2
client.delete(0..2)

End-less ranges also span to the end of the list:

# Delete all songs from the playlist starting from index 10
client.delete(10..)
# or using exclusive range (same effect)
client.delete(10...)

Begin-less ranges default the start to 0:

# Delete songs 0, 1, and 2
client.delete(..2)

# Delete songs 0 and 1
client.delete(...2)

Filters

Filters documentation

All commands which search for songs (playlistsearch, playlistfind, searchaddpl, searchcount, searchplaylist, list, count, find, search, findadd, searchadd) share a common filter syntax.

The find commands are case sensitive, which search and related commands ignore case.

client.search("(any =~ 'crystal')")
client.searchaddpl("alt_rock", "(genre == 'Alternative Rock')", sort: "-ArtistSort", window: (5..10))
client.list("filename", "((artist == 'Linkin Park') AND (date == '2003'))")

Build MPD query expressions in Crystal

The MPD::Filter class helps you construct complex MPD filter expressions using a fluent and chainable DSL β€” fully compatible with MPD filter syntax.

You can build expressions using chainable methods like #eq, #contains, #not_eq, and logical #not.

filter =
  MPD::Filter
    .eq("Artist", "Linkin Park")
    .contains("Album", "Meteora")
    .not_eq("Title", "Numb")
    .sort("Track")
    .window(..10)

client.find(filter)

This is equivalent to:

expression = "((Artist == 'Linkin Park') AND (Album contains 'Meteora') AND (Title != 'Numb'))"

client.find(expression, sort: "Track", window: ..10)

You can also use this block-based filter DSL like:

client.search do |filter|
  filter
    .eq(:artist, "Linkin Park")
    .match(:album, "Meteora.*")
    .not_eq(:title, "Numb")
    .sort(:track)
    .window(..10)
end
Supported methods
Method MPD Equivalent
eq(tag, value) (tag == 'value')
not_eq(tag, value) (tag != 'value')
match(tag, value) (tag =~ 'value')
not_match(tag, value) (tag !~ 'value')
eq_cs(tag, value) (tag eq_cs 'value')
eq_ci(tag, value) (tag eq_ci 'value')
not_eq_cs(tag, value) (!(tag eq_cs 'value'))
not_eq_ci(tag, value) (!(tag eq_ci 'value'))
contains(tag, value) (tag contains 'value')
not_contains(tag, value) (!(tag contains 'value'))
contains_cs(tag, value) (tag contains_cs 'value')
contains_ci(tag, value) (tag contains_ci 'value')
not_contains_cs(tag, value) (!(tag contains_cs 'value'))
not_contains_ci(tag, value) (!(tag contains_ci 'value'))
starts_with(tag, value) (tag starts_with 'value')
not_starts_with(tag, value) (!(tag starts_with 'value'))
starts_with_cs(tag, value) (tag starts_with_cs 'value')
starts_with_ci(tag, value) (tag starts_with_ci 'value')
not_starts_with_cs(tag,val) (!(tag starts_with_cs 'value'))
not_starts_with_ci(tag,val) (!(tag starts_with_ci 'value'))
not(filter) (!...)

Chaining multiple filters implies logical AND.

Negate an expression with #not.

inner = MPD::Filter.eq("Genre", "Pop")
outer = MPD::Filter.not(inner)
# => "(!(Genre == \"Pop\"))"

which is equivalent to

MPD::Filter.not_eq("Genre", "Pop")
# => "(Genre != \"Pop\")"

Callbacks

Callbacks are a simple way to make your client respond to events, rather that have to continuously ask the server for updates. This is done by having a background thread continuously check the server for changes.

To make use of callbacks, you need to:

  1. Create a MPD client instance with callbacks enabled.

    client = MPD::Client.new(with_callbacks: true)
  2. Setup a callback to be called when something happens.

    client.on :state do |state|
      puts "[#{Time.local}] State was change to #{state}"
    end

crystal_mpd supports callbacks for any of the keys returned by MPD::Client#status.

Here's the full list of events:

  • :partition
  • :volume
  • :repeat
  • :random
  • :single
  • :consume
  • :playlist
  • :playlistlength
  • :state
  • :song
  • :songid
  • :nextsong
  • :nextsongid
  • :time
  • :elapsed
  • :duration
  • :bitrate
  • :xfade
  • :mixrampdb
  • :mixrampdelay
  • :audio
  • :updating_db
  • :error
  • :lastloadedplaylist
client = MPD::Client.new(with_callbacks: true)
client.callbacks_timeout = 2.seconds

client.on :state do |state|
  puts "[#{Time.local}] State was change to #{state}"
end

client.on :song do
  if (current_song = client.currentsong)
    puts "[#{Time.local}] 🎡 #{current_song["Artist"]} - #{current_song["Title"]}"
  end
end

# Keep the program running
loop { sleep 1.second }

The above will connect to the server like normal, but this time it will create a new thread that loops until you issue an exit. This loop checks the server, then sleeps for 2 seconds, then loops.

In addition to registering individual event listeners using #on, the MPD client also supports a global callback listener using #on_callback.

This method allows you to handle all events in a single block and react based on the event type.

client = MPD::Client.new(with_callbacks: true)

client.on_callback do |event, value|
  case event
  when .state?
    puts "State changed to #{value}"
  when .song?
    puts "Now playing: #{value}"
  when .repeat?
    puts "Repeat mode: #{value == "1" ? "On" : "Off"}"
  else
    puts "[#{event}] β†’ #{value}"
  end
end

# Keep the program running
loop { sleep 1.second }

You can combine #on_callback with specific #on handlers. For example:

client.on(:state) { |val| puts "STATE: #{val}" }

client.on_callback do |event, value|
  puts "[ALL EVENTS] #{event} => #{value}"
end

Binary responses

Some commands can return binary data.

client = MPD::Client.new

if (current_song = client.currentsong)
  if (response = client.albumart(current_song["file"]))
    data, binary = response
    # data # => {"size" => "30219", "type" => "image/jpeg", "binary" => "5643"}

    extension = MIME.extensions(data["type"]).first? || ".png"

    file = File.open("cover#{extension}", "w")
    file.write(binary.to_slice)
  end
end

The above will locate album art for the current song and save image to cover.jpg file.

Client-to-Client communication

crystal_mpd supports MPD's built-in client-to-client messaging system via channels. This allows clients to exchange messages in real time through the MPD server.

Supported Methods

client.subscribe("my_channel")          # Subscribes to a channel
client.unsubscribe("my_channel")        # Unsubscribes from a channel
client.channels                         # Returns a list of all existing channels
client.readmessages                     # Reads messages sent to subscribed channels
client.sendmessage("my_channel", "Hi!") # Sends a message to a specific channel

Example

client.subscribe("notifications")

# Somewhere else, another client sends a message
client.sendmessage("notifications", "System update available")

# The first client reads the message
messages = client.readmessages
puts messages
# => [{"channel" => "notifications", "message" => "System update available"}]

Logging

require "crystal_mpd"

client = MPD::Client.new

MPD::Log.level = :debug
MPD::Log.backend = ::Log::IOBackend.new

Development

Install dependencies:

shards

To run test:

crystal spec

Who's using MPD::Client

If you're using MPD::Client and would like to have your application added to this list, just submit a PR!

  • cryMPD - control MPD audio playing in the browser

Contributing

  1. Fork it (https://github.com/mamantoha/crystal_mpd/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

  • mamantoha Anton Maminov - creator, maintainer

License

Copyright: 2018-2025 Anton Maminov ([email protected])

This library is distributed under the MIT license. Please see the LICENSE file.

About

Concurrent MPD client written in Crystal

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •