{"id":15683655,"url":"https://github.com/mamantoha/crystal_mpd","last_synced_at":"2025-05-07T15:01:19.802Z","repository":{"id":37749036,"uuid":"118741223","full_name":"mamantoha/crystal_mpd","owner":"mamantoha","description":"Concurrent MPD client written in Crystal","archived":false,"fork":false,"pushed_at":"2025-04-14T15:57:01.000Z","size":317,"stargazers_count":10,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-14T16:51:20.323Z","etag":null,"topics":["crystal","hacktoberfest","mpd","mpd-client","music-player-daemon"],"latest_commit_sha":null,"homepage":"https://mamantoha.github.io/crystal_mpd/","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mamantoha.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2018-01-24T09:15:22.000Z","updated_at":"2025-04-14T15:56:37.000Z","dependencies_parsed_at":"2024-10-23T16:45:27.579Z","dependency_job_id":"d35fa495-4d6f-44cc-8ba8-714439e315aa","html_url":"https://github.com/mamantoha/crystal_mpd","commit_stats":{"total_commits":164,"total_committers":2,"mean_commits":82.0,"dds":"0.018292682926829285","last_synced_commit":"4d1f15cdd0b219ed07127071c1c13e8945bf96c0"},"previous_names":[],"tags_count":29,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mamantoha%2Fcrystal_mpd","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mamantoha%2Fcrystal_mpd/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mamantoha%2Fcrystal_mpd/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mamantoha%2Fcrystal_mpd/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mamantoha","download_url":"https://codeload.github.com/mamantoha/crystal_mpd/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252902604,"owners_count":21822259,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crystal","hacktoberfest","mpd","mpd-client","music-player-daemon"],"created_at":"2024-10-03T17:08:02.580Z","updated_at":"2025-05-07T15:01:19.639Z","avatar_url":"https://github.com/mamantoha.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MPD::Client\n\n![Crystal CI](https://github.com/mamantoha/crystal_mpd/workflows/Crystal%20CI/badge.svg)\n[![GitHub release](https://img.shields.io/github/release/mamantoha/crystal_mpd.svg)](https://github.com/mamantoha/crystal_mpd/releases)\n[![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](https://mamantoha.github.io/crystal_mpd/)\n[![License](https://img.shields.io/github/license/mamantoha/crystal_mpd.svg)](https://github.com/mamantoha/crystal_mpd/blob/master/LICENSE)\n\nConcurrent [Music Player Daemon](https://www.musicpd.org/) client written entirely in Crystal.\n\n## Main features\n\n- Filtering DSL\n- Range support\n- Callbacks\n- Command lists support\n- Binary responses\n- Client to client communicate\n- Logging\n- Handle exceptions\n\n## Installation\n\nAdd this to your application's `shard.yml`:\n\n```yaml\ndependencies:\n  crystal_mpd:\n    github: mamantoha/crystal_mpd\n```\n\n## Usage\n\n```crystal\nrequire \"crystal_mpd\"\n```\n\nAll functionality is contained in the `MPD::Client` class.\nCreating an instance of this class is as simple as:\n\n```crystal\nclient = MPD::Client.new(\"localhost\", 6600)\n```\n\nYou can also omit the `host` and `port`, and it will use the defaults:\n\n```crystal\nclient = MPD::Client.new(\"localhost\")\nclient = MPD::Client.new\n```\n\nYou can connect to a local socket (UNIX domain socket), specify an absolute path:\n\n```crystal\nclient = MPD::Client.new(\"/run/mpd/socket\")\n```\n\nIf a password specified for access to MPD:\n\n```crystal\nclient = MPD::Client.new(\"localhost\", password: \"password\")\n```\n\nThe client library can be used as follows:\n\n```crystal\nputs client.version # print the mpd version\nclient.play(2)      # begins playing the playlist at song number 2\nputs client.status  # print the current status of the player and the volume level\nclient.close        # send the close command\nclient.disconect    # disconnect from the server\n```\n\nCheck `MPD::Client` [source](https://mamantoha.github.io/crystal_mpd/MPD/Client.html) for supported commands.\n\nTo use all `crystal_mpd` functions you should use the latest stable MPD version (0.24.x).\n\n### Command lists\n\nCommand lists [documentation](https://mpd.readthedocs.io/en/latest/protocol.html#command-lists).\n\nTo facilitate faster adding of files etc. you can pass a list of commands all at once using a command list.\nThe command list begins with `command_list_ok_begin` and ends with `command_list_end`.\n\nIt does not execute any commands until the list has ended.\nThe return value is whatever the return for a list of commands is. On success for all commands, `OK` is returned.\n\nIf a command fails, no more commands are executed and the appropriate ACK error is returned.\n\nIf `command_list_ok_begin` is used, `list_OK` is returned for each successful command executed in the command list.\n\n```crystal\nclient.command_list_ok_begin # start a command list\nclient.update                # insert the update command into the list\nclient.status                # insert the status command into the list\nclient.command_list_end      # result will be a Array with the results\n```\n\nor\n\n```crystal\nclient.with_command_list do\n  client.update\n  client.status\nend\n```\n\n### Ranges\n\nRanges [documentation](https://mpd.readthedocs.io/en/latest/protocol.html#ranges).\n\nSome 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.\n\nNote: MPD treats `END` as exclusive, so we internally adjust inclusive ranges to match this behavior.\nAlso note that in MPD, song indexes start at 0 — the same as in Crystal.\n\n```crystal\n# Move first 3 songs to position 10, 11, and 12\nclient.move(0..2, 10)\n\n# Delete songs 0 and 1 (but NOT 2)\nclient.delete(0...2)\n\n# Delete songs 0, 1, and 2\nclient.delete(0..2)\n```\n\nEnd-less ranges also span to the end of the list:\n\n```crystal\n# Delete all songs from the playlist starting from index 10\nclient.delete(10..)\n# or using exclusive range (same effect)\nclient.delete(10...)\n```\n\nBegin-less ranges default the start to 0:\n\n```crystal\n# Delete songs 0, 1, and 2\nclient.delete(..2)\n\n# Delete songs 0 and 1\nclient.delete(...2)\n```\n\n### Filters\n\nFilters [documentation](https://mpd.readthedocs.io/en/latest/protocol.html#filters)\n\nAll commands which search for songs (`playlistsearch`, `playlistfind`, `searchaddpl`, `searchcount`, `searchplaylist`, `list`, `count`, `find`, `search`, `findadd`, `searchadd`) share a common filter syntax.\n\nThe `find` commands are case sensitive, which `search` and related commands ignore case.\n\n```crystal\nclient.search(\"(any =~ 'crystal')\")\nclient.searchaddpl(\"alt_rock\", \"(genre == 'Alternative Rock')\", sort: \"-ArtistSort\", window: (5..10))\nclient.list(\"filename\", \"((artist == 'Linkin Park') AND (date == '2003'))\")\n```\n\n#### Build MPD query expressions in Crystal\n\nThe `MPD::Filter` class helps you construct complex MPD filter expressions using a fluent and chainable DSL — fully compatible with MPD filter syntax.\n\nYou can build expressions using chainable methods like `#eq`, `#contains`, `#not_eq`, and logical `#not`.\n\n```crystal\nfilter =\n  MPD::Filter\n    .eq(\"Artist\", \"Linkin Park\")\n    .contains(\"Album\", \"Meteora\")\n    .not_eq(\"Title\", \"Numb\")\n    .sort(\"Track\")\n    .window(..10)\n\nclient.find(filter)\n```\n\nThis is equivalent to:\n\n```crystal\nexpression = \"((Artist == 'Linkin Park') AND (Album contains 'Meteora') AND (Title != 'Numb'))\"\n\nclient.find(expression, sort: \"Track\", window: ..10)\n```\n\nYou can also use this block-based filter DSL like:\n\n```crystal\nclient.search do |filter|\n  filter\n    .eq(:artist, \"Linkin Park\")\n    .match(:album, \"Meteora.*\")\n    .not_eq(:title, \"Numb\")\n    .sort(:track)\n    .window(..10)\nend\n```\n\n##### Supported methods\n\n| Method                        | MPD Equivalent                    |\n| ----------------------------- | --------------------------------- |\n| `eq(tag, value)`              | `(tag == 'value')`                |\n| `not_eq(tag, value)`          | `(tag != 'value')`                |\n| `match(tag, value)`           | `(tag =~ 'value')`                |\n| `not_match(tag, value)`       | `(tag !~ 'value')`                |\n| `eq_cs(tag, value)`           | `(tag eq_cs 'value')`             |\n| `eq_ci(tag, value)`           | `(tag eq_ci 'value')`             |\n| `not_eq_cs(tag, value)`       | `(!(tag eq_cs 'value'))`          |\n| `not_eq_ci(tag, value)`       | `(!(tag eq_ci 'value'))`          |\n| `contains(tag, value)`        | `(tag contains 'value')`          |\n| `not_contains(tag, value)`    | `(!(tag contains 'value'))`       |\n| `contains_cs(tag, value)`     | `(tag contains_cs 'value')`       |\n| `contains_ci(tag, value)`     | `(tag contains_ci 'value')`       |\n| `not_contains_cs(tag, value)` | `(!(tag contains_cs 'value'))`    |\n| `not_contains_ci(tag, value)` | `(!(tag contains_ci 'value'))`    |\n| `starts_with(tag, value)`     | `(tag starts_with 'value')`       |\n| `not_starts_with(tag, value)` | `(!(tag starts_with 'value'))`    |\n| `starts_with_cs(tag, value)`  | `(tag starts_with_cs 'value')`    |\n| `starts_with_ci(tag, value)`  | `(tag starts_with_ci 'value')`    |\n| `not_starts_with_cs(tag,val)` | `(!(tag starts_with_cs 'value'))` |\n| `not_starts_with_ci(tag,val)` | `(!(tag starts_with_ci 'value'))` |\n| `not(filter)`                 | `(!...)`                          |\n\nChaining multiple filters implies logical `AND`.\n\nNegate an expression with `#not`.\n\n```crystal\ninner = MPD::Filter.eq(\"Genre\", \"Pop\")\nouter = MPD::Filter.not(inner)\n# =\u003e \"(!(Genre == \\\"Pop\\\"))\"\n```\n\nwhich is equivalent to\n\n```crystal\nMPD::Filter.not_eq(\"Genre\", \"Pop\")\n# =\u003e \"(Genre != \\\"Pop\\\")\"\n```\n\n### Callbacks\n\nCallbacks 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.\n\nTo make use of callbacks, you need to:\n\n1. Create a MPD client instance with callbacks enabled.\n\n   ```crystal\n   client = MPD::Client.new(with_callbacks: true)\n   ```\n\n2. Setup a callback to be called when something happens.\n\n   ```crystal\n   client.on :state do |state|\n     puts \"[#{Time.local}] State was change to #{state}\"\n   end\n   ```\n\n`crystal_mpd` supports callbacks for any of the keys returned by `MPD::Client#status`.\n\nHere's the full list of events:\n\n- `:partition`\n- `:volume`\n- `:repeat`\n- `:random`\n- `:single`\n- `:consume`\n- `:playlist`\n- `:playlistlength`\n- `:state`\n- `:song`\n- `:songid`\n- `:nextsong`\n- `:nextsongid`\n- `:time`\n- `:elapsed`\n- `:duration`\n- `:bitrate`\n- `:xfade`\n- `:mixrampdb`\n- `:mixrampdelay`\n- `:audio`\n- `:updating_db`\n- `:error`\n- `:lastloadedplaylist`\n\n```crystal\nclient = MPD::Client.new(with_callbacks: true)\nclient.callbacks_timeout = 2.seconds\n\nclient.on :state do |state|\n  puts \"[#{Time.local}] State was change to #{state}\"\nend\n\nclient.on :song do\n  if (current_song = client.currentsong)\n    puts \"[#{Time.local}] 🎵 #{current_song[\"Artist\"]} - #{current_song[\"Title\"]}\"\n  end\nend\n\n# Keep the program running\nloop { sleep 1.second }\n```\n\nThe above will connect to the server like normal, but this time it will create a new thread\nthat loops until you issue an exit. This loop checks the server, then sleeps for 2 seconds, then loops.\n\nIn addition to registering individual event listeners using `#on`, the MPD client also supports a global callback listener using `#on_callback`.\n\nThis method allows you to handle all events in a single block and react based on the event type.\n\n```crystal\nclient = MPD::Client.new(with_callbacks: true)\n\nclient.on_callback do |event, value|\n  case event\n  when .state?\n    puts \"State changed to #{value}\"\n  when .song?\n    puts \"Now playing: #{value}\"\n  when .repeat?\n    puts \"Repeat mode: #{value == \"1\" ? \"On\" : \"Off\"}\"\n  else\n    puts \"[#{event}] → #{value}\"\n  end\nend\n\n# Keep the program running\nloop { sleep 1.second }\n```\n\nYou can combine `#on_callback` with specific `#on` handlers. For example:\n\n```crystal\nclient.on(:state) { |val| puts \"STATE: #{val}\" }\n\nclient.on_callback do |event, value|\n  puts \"[ALL EVENTS] #{event} =\u003e #{value}\"\nend\n```\n\n### Binary responses\n\nSome commands can return binary data.\n\n```crystal\nclient = MPD::Client.new\n\nif (current_song = client.currentsong)\n  if (response = client.albumart(current_song[\"file\"]))\n    data, binary = response\n    # data # =\u003e {\"size\" =\u003e \"30219\", \"type\" =\u003e \"image/jpeg\", \"binary\" =\u003e \"5643\"}\n\n    extension = MIME.extensions(data[\"type\"]).first? || \".png\"\n\n    file = File.open(\"cover#{extension}\", \"w\")\n    file.write(binary.to_slice)\n  end\nend\n```\n\nThe above will locate album art for the current song and save image to `cover.jpg` file.\n\n### Client-to-Client communication\n\n`crystal_mpd` supports MPD's built-in client-to-client messaging system via channels.\nThis allows clients to exchange messages in real time through the MPD server.\n\n#### Supported Methods\n\n```crystal\nclient.subscribe(\"my_channel\")          # Subscribes to a channel\nclient.unsubscribe(\"my_channel\")        # Unsubscribes from a channel\nclient.channels                         # Returns a list of all existing channels\nclient.readmessages                     # Reads messages sent to subscribed channels\nclient.sendmessage(\"my_channel\", \"Hi!\") # Sends a message to a specific channel\n```\n\n#### Example\n\n```crystal\nclient.subscribe(\"notifications\")\n\n# Somewhere else, another client sends a message\nclient.sendmessage(\"notifications\", \"System update available\")\n\n# The first client reads the message\nmessages = client.readmessages\nputs messages\n# =\u003e [{\"channel\" =\u003e \"notifications\", \"message\" =\u003e \"System update available\"}]\n```\n\n### Logging\n\n```crystal\nrequire \"crystal_mpd\"\n\nclient = MPD::Client.new\n\nMPD::Log.level = :debug\nMPD::Log.backend = ::Log::IOBackend.new\n```\n\n## Development\n\nInstall dependencies:\n\n```console\nshards\n```\n\nTo run test:\n\n```console\ncrystal spec\n```\n\n## Who's using `MPD::Client`\n\nIf you're using `MPD::Client` and would like to have your application added to this list, just submit a PR!\n\n- [cryMPD](https://github.com/mamantoha/cryMPD) - control MPD audio playing in the browser\n\n## Contributing\n\n1. Fork it (\u003chttps://github.com/mamantoha/crystal_mpd/fork\u003e)\n2. Create your feature branch (git checkout -b my-new-feature)\n3. Commit your changes (git commit -am 'Add some feature')\n4. Push to the branch (git push origin my-new-feature)\n5. Create a new Pull Request\n\n## Contributors\n\n- [mamantoha](https://github.com/mamantoha) Anton Maminov - creator, maintainer\n\n## License\n\nCopyright: 2018-2025 Anton Maminov (\u003canton.maminov@gmail.com\u003e)\n\nThis library is distributed under the MIT license. Please see the LICENSE file.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmamantoha%2Fcrystal_mpd","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmamantoha%2Fcrystal_mpd","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmamantoha%2Fcrystal_mpd/lists"}