From 18adeb829d37026e0a3693f05eaa77759d73f9a8 Mon Sep 17 00:00:00 2001 From: Anton Maminov Date: Thu, 5 May 2022 21:39:50 +0300 Subject: [PATCH 01/14] fetch binary --- lib/mpd_client.rb | 60 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/lib/mpd_client.rb b/lib/mpd_client.rb index 82b9655..6a41dd7 100644 --- a/lib/mpd_client.rb +++ b/lib/mpd_client.rb @@ -1,13 +1,14 @@ # 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 @@ -241,6 +242,10 @@ def log # Sets the +logger+ used by this instance of MPD::Client attr_writer :log + def albumart(uri) + fetch_binary(StringIO.new, 0, 'albumart', uri) + end + private def ensure_connected @@ -286,10 +291,10 @@ def write_command(command, *args) 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 @@ -309,10 +314,7 @@ 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 + line.split(separator, 2) end def read_pairs(separator = ': ') @@ -343,6 +345,8 @@ def fetch_list 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? @@ -358,14 +362,18 @@ def fetch_list 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 @@ -380,6 +388,41 @@ def fetch_object 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 @@ -419,6 +462,7 @@ def fetch_playlists def fetch_playlist result = [] read_pairs(':').each do |_key, value| + value = value.chomp.force_encoding('utf-8') result << value end From 2070c73ed87cb473214eb42a19b4bd72013abbdd Mon Sep 17 00:00:00 2001 From: Anton Maminov Date: Thu, 5 May 2022 21:40:56 +0300 Subject: [PATCH 02/14] Update mpd_client.gemspec --- mpd_client.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/mpd_client.gemspec b/mpd_client.gemspec index 195c492..ccc7320 100644 --- a/mpd_client.gemspec +++ b/mpd_client.gemspec @@ -21,4 +21,5 @@ Gem::Specification.new do |gem| gem.license = 'MIT' gem.add_development_dependency 'bundler' + gem.metadata['rubygems_mfa_required'] = 'true' end From 84303a2ce81848e314b9c04c38d9635abc38eb70 Mon Sep 17 00:00:00 2001 From: Anton Maminov Date: Thu, 5 May 2022 21:43:47 +0300 Subject: [PATCH 03/14] Create albumart.rb --- examples/albumart.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 examples/albumart.rb diff --git a/examples/albumart.rb b/examples/albumart.rb new file mode 100644 index 0000000..5090ca5 --- /dev/null +++ b/examples/albumart.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'bundler' +Bundler.setup :default + +require 'logger' +require 'mpd_client' + +# MPD::Client.log = Logger.new($stderr) + +client = MPD::Client.new +client.connect('localhost', 6600) + +if (current_song = client.currentsong) + data, io = client.albumart(current_song['file']) + puts data + File.write('cover.jpg', io.string) +end From d11e4a76f6d68b65437944e5b17646a281923967 Mon Sep 17 00:00:00 2001 From: Anton Maminov Date: Thu, 5 May 2022 21:53:17 +0300 Subject: [PATCH 04/14] cosmetic changes --- lib/mpd_client.rb | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/lib/mpd_client.rb b/lib/mpd_client.rb index 6a41dd7..c57b9dc 100644 --- a/lib/mpd_client.rb +++ b/lib/mpd_client.rb @@ -161,6 +161,7 @@ def connect(host = 'localhost', port = 6600) def add_command(name, retval) escaped_name = name.tr(' ', '_') + define_method escaped_name.to_sym do |*args| ensure_connected @@ -190,11 +191,12 @@ def connect(host = 'localhost', port = 6600) 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 @@ -223,6 +225,7 @@ def command_list_ok_begin raise 'Already in command list' unless @command_list.nil? write_command('command_list_ok_begin') + @command_list = [] end @@ -271,20 +274,24 @@ def write_line(line) 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(' ')) @@ -312,6 +319,7 @@ def read_line def read_pair(separator) line = read_line + return if line.nil? line.split(separator, 2) @@ -319,7 +327,9 @@ def read_pair(separator) def read_pairs(separator = ': ') result = [] + pair = read_pair(separator) + while pair result << pair pair = read_pair(separator) @@ -330,6 +340,7 @@ def read_pairs(separator = ': ') def fetch_item pairs = read_pairs + return nil if pairs.size != 1 pairs[0][1] @@ -337,6 +348,7 @@ def fetch_item def fetch_nothing line = read_line + raise "Got unexpected value: #{line}" unless line.nil? end @@ -461,6 +473,7 @@ def fetch_playlists def fetch_playlist result = [] + read_pairs(':').each do |_key, value| value = value.chomp.force_encoding('utf-8') result << value @@ -471,6 +484,7 @@ def fetch_playlist def fetch_stickers result = [] + read_pairs.each do |_key, sticker| value = sticker.split('=', 2) raise "Could now parse sticker: #{sticker}" if value.size < 2 @@ -487,6 +501,7 @@ def fetch_sticker def fetch_command_list result = [] + begin @command_list.each do |retval| result << (eval retval) @@ -500,9 +515,11 @@ def fetch_command_list 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] From ec37aaf5f81c405f982607734d04eac26eea88f1 Mon Sep 17 00:00:00 2001 From: Anton Maminov Date: Thu, 5 May 2022 21:57:42 +0300 Subject: [PATCH 05/14] prepare 0.2.0 --- .travis.yml | 6 +++--- CHANGELOG.md | 4 ++++ lib/mpd_client/version.rb | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index f1a5a8f..d0ab184 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ language: ruby rvm: - - 3.0.0 - - 2.7.2 - - 2.6.6 + - 3.1.2 + - 3.0.4 + - 2.7.6 script: - rspec diff --git a/CHANGELOG.md b/CHANGELOG.md index bea2963..b2f47c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # MPD::Client CHANGELOG +## 0.2.0 + +* Tested with Ruby 3.1 +* Add `albumart` command ## 0.1.0 * Rename `MPDClient` to `MPD::Client` diff --git a/lib/mpd_client/version.rb b/lib/mpd_client/version.rb index dd7e7b5..ce99ef8 100644 --- a/lib/mpd_client/version.rb +++ b/lib/mpd_client/version.rb @@ -2,6 +2,6 @@ module MPD class Client - VERSION = '0.1.1' + VERSION = '0.2.0' end end From a002d470c7aebe6d5c5c400d3b0edf7651657ebd Mon Sep 17 00:00:00 2001 From: Anton Maminov Date: Thu, 5 May 2022 21:58:47 +0300 Subject: [PATCH 06/14] 2022 --- LICENSE | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 49a7f85..e46750d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012 Anton Maminov +Copyright (c) 2012-2022 Anton Maminov MIT License @@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index bfd5ed9..0033700 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,6 @@ To install this gem onto your local machine, run `bundle exec rake install`. To ## License and Author -Copyright (c) 2013-2018 by Anton Maminov +Copyright (c) 2012-2022 by Anton Maminov This library is distributed under the MIT license. Please see the LICENSE file. From 1ad1ffc2ae476b4ccf2895d9b112987dd7e25c6e Mon Sep 17 00:00:00 2001 From: Anton Maminov Date: Thu, 5 May 2022 22:05:24 +0300 Subject: [PATCH 07/14] Update README.md --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 0033700..2ff751e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ gem install mpd_client All functionality is contained in the `MPD::Client` class. Creating an instance of this class is as simple as: ```ruby +require 'mpd_client' + client = MPD::Client.new ``` @@ -64,6 +66,26 @@ client.status # insert the status command into the list client.command_list_end # result will be a Array with the results ``` +### Binary responses + +Some commands can return binary data. + +```ruby +require 'mpd_client' + +client = MPD::Client.new +client.connect('localhost', 6600) + +if (current_song = client.currentsong) + data, io = client.albumart(current_song['file']) + io # StringIO + data # => {"size"=>"322860", "binary"=>"3372"} + File.write('cover.jpg', io.string) +end +``` + +The above will locate album art for the current song and save image to `cover.jpg` file. + ### Ranges Some commands(e.g. `move`, `delete`, `load`, `shuffle`, `playlistinfo`) support integer ranges(`[START:END]`) as argument. This is done in `mpd_client` by using two element array: From b90d88a37fe0c4bff1915d320af59a5fb530c865 Mon Sep 17 00:00:00 2001 From: Anton Maminov Date: Fri, 6 May 2022 16:00:57 +0300 Subject: [PATCH 08/14] add readpicture --- CHANGELOG.md | 2 ++ README.md | 4 ++-- examples/albumart.rb | 2 +- lib/mpd_client.rb | 4 ++++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2f47c9..cae0d04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * Tested with Ruby 3.1 * Add `albumart` command +* Add `readpicture` command + ## 0.1.0 * Rename `MPDClient` to `MPD::Client` diff --git a/README.md b/README.md index 2ff751e..497a285 100644 --- a/README.md +++ b/README.md @@ -77,9 +77,9 @@ client = MPD::Client.new client.connect('localhost', 6600) if (current_song = client.currentsong) - data, io = client.albumart(current_song['file']) + data, io = client.readpicture(current_song['file']) io # StringIO - data # => {"size"=>"322860", "binary"=>"3372"} + data # => {"size"=>"322860", "type"=>"image/jpeg", "binary"=>"3372"} File.write('cover.jpg', io.string) end ``` diff --git a/examples/albumart.rb b/examples/albumart.rb index 5090ca5..6e81635 100644 --- a/examples/albumart.rb +++ b/examples/albumart.rb @@ -12,7 +12,7 @@ client.connect('localhost', 6600) if (current_song = client.currentsong) - data, io = client.albumart(current_song['file']) + data, io = client.readpicture(current_song['file']) puts data File.write('cover.jpg', io.string) end diff --git a/lib/mpd_client.rb b/lib/mpd_client.rb index c57b9dc..b38076e 100644 --- a/lib/mpd_client.rb +++ b/lib/mpd_client.rb @@ -249,6 +249,10 @@ 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 From bb15353331e25ead9d0d8251e06fb89a21ed732b Mon Sep 17 00:00:00 2001 From: Anton Maminov Date: Fri, 6 May 2022 16:27:49 +0300 Subject: [PATCH 09/14] add solargraph --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index 12e41e1..0f11a4b 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gemspec group :development, :test do gem 'pry' gem 'rubocop' + gem 'solargraph' end group :test do From d575e23971587d0262668aed46da97c6766a3f4d Mon Sep 17 00:00:00 2001 From: Anton Maminov Date: Fri, 6 May 2022 16:42:27 +0300 Subject: [PATCH 10/14] remove playlist command, instead use playlistinfo --- lib/mpd_client.rb | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/lib/mpd_client.rb b/lib/mpd_client.rb index b38076e..cb16f94 100644 --- a/lib/mpd_client.rb +++ b/lib/mpd_client.rb @@ -53,7 +53,6 @@ module MPD 'deleteid' => 'fetch_nothing', 'move' => 'fetch_nothing', 'moveid' => 'fetch_nothing', - 'playlist' => 'fetch_playlist', 'playlistfind' => 'fetch_songs', 'playlistid' => 'fetch_songs', 'playlistinfo' => 'fetch_songs', @@ -321,22 +320,22 @@ def read_line line end - def read_pair(separator) + def read_pair line = read_line return if line.nil? - line.split(separator, 2) + 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 @@ -475,17 +474,6 @@ def fetch_playlists fetch_objects(['playlist']) end - def fetch_playlist - result = [] - - read_pairs(':').each do |_key, value| - value = value.chomp.force_encoding('utf-8') - result << value - end - - result - end - def fetch_stickers result = [] From 69c0c50b750a160961ea74205e4b560f99ec9fb3 Mon Sep 17 00:00:00 2001 From: Anton Maminov Date: Fri, 6 May 2022 16:44:46 +0300 Subject: [PATCH 11/14] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cae0d04..d2614a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Tested with Ruby 3.1 * Add `albumart` command * Add `readpicture` command +* Remove `playlist` command. Use `playlistinfo` instead ## 0.1.0 From 285ea86b1432151f3b5133a9059c953748a6fa5e Mon Sep 17 00:00:00 2001 From: Anton Maminov Date: Fri, 6 May 2022 16:44:50 +0300 Subject: [PATCH 12/14] Create client.rb --- examples/client.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 examples/client.rb diff --git a/examples/client.rb b/examples/client.rb new file mode 100644 index 0000000..7c21868 --- /dev/null +++ b/examples/client.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'bundler' +Bundler.setup :default + +require 'logger' +require 'mpd_client' + +MPD::Client.log = Logger.new($stderr) + +client = MPD::Client.new +client.connect('localhost', 6600) + +puts client.currentsong From 3b5b5699245effefd22b06c17937411956abbbd7 Mon Sep 17 00:00:00 2001 From: Anton Maminov Date: Fri, 6 May 2022 16:50:53 +0300 Subject: [PATCH 13/14] Update client.rb --- examples/client.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/client.rb b/examples/client.rb index 7c21868..a5bb0d5 100644 --- a/examples/client.rb +++ b/examples/client.rb @@ -11,4 +11,7 @@ client = MPD::Client.new client.connect('localhost', 6600) +puts client.stats +puts client.status puts client.currentsong +puts client.playlistinfo From 3c2fe63ec46f544bc96c6918e706eea2c41d891f Mon Sep 17 00:00:00 2001 From: Anton Maminov Date: Fri, 6 May 2022 17:58:13 +0300 Subject: [PATCH 14/14] fix links in docs and comments --- MPD_COMMANDS.md | 2 +- README.md | 2 +- examples/stickers.rb | 2 +- lib/mpd_client.rb | 5 ++--- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/MPD_COMMANDS.md b/MPD_COMMANDS.md index 5d7eb38..ac108ce 100644 --- a/MPD_COMMANDS.md +++ b/MPD_COMMANDS.md @@ -102,7 +102,7 @@ If the optional `SUBSYSTEMS` argument is used, MPD will only send notifications --- `mixrampdb {deciBels} => fetch_nothing` -> Sets the threshold at which songs will be overlapped. Like crossfading but doesn't fade the track volume, just overlaps. The songs need to have MixRamp tags added by an external tool. 0dB is the normalized maximum volume so use negative values, I prefer -17dB. In the absence of mixramp tags crossfading will be used. See [mixramp](https://sourceforge.net/projects/mixramp/) +> Sets the threshold at which songs will be overlapped. Like crossfading but doesn't fade the track volume, just overlaps. The songs need to have MixRamp tags added by an external tool. 0dB is the normalized maximum volume so use negative values, I prefer -17dB. In the absence of mixramp tags crossfading will be used. See [mixramp](https://mpd.readthedocs.io/en/latest/user.html?highlight=mixramp#mixramp) --- `mixrampdelay {SECONDS} => fetch_nothing` diff --git a/README.md b/README.md index 497a285..8c64fdd 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ client = MPD::Client.new client.log = Logger.new($stderr) ``` -For more information about logging configuration, see [Logger](https://ruby-doc.org/stdlib-2.5.1/libdoc/logger/rdoc/Logger.html) +For more information about logging configuration, see [Logger](https://ruby-doc.org/stdlib/libdoc/logger/rdoc/Logger.html) ## Development diff --git a/examples/stickers.rb b/examples/stickers.rb index 6908f08..653e245 100644 --- a/examples/stickers.rb +++ b/examples/stickers.rb @@ -9,7 +9,7 @@ MPD::Client.log = Logger.new($stderr) # Stickers -# http://www.musicpd.org/doc/protocol/ch03s07.html +# https://mpd.readthedocs.io/en/latest/protocol.html#stickers client = MPD::Client.new diff --git a/lib/mpd_client.rb b/lib/mpd_client.rb index cb16f94..6c2c82d 100644 --- a/lib/mpd_client.rb +++ b/lib/mpd_client.rb @@ -10,9 +10,8 @@ module MPD 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',