From e9bbf620500b6ea3441a4e8ce91fd2f656230d4f Mon Sep 17 00:00:00 2001 From: Gavin Kistner Date: Wed, 17 Feb 2016 17:27:51 -0700 Subject: [PATCH 1/9] Command Lists built (only partially tested); includes socket spoofing test framework --- lib/ruby-mpd.rb | 34 ++-- lib/ruby-mpd/parser.rb | 12 +- lib/ruby-mpd/plugins/command_list.rb | 177 ++++++++++++++++++ lib/ruby-mpd/plugins/database.rb | 5 +- lib/ruby-mpd/plugins/stickers.rb | 5 +- test/record_sample_responses.rb | 16 ++ .../socket_recordings/commandlist-clear_addid | 11 ++ test/socket_recordings/commandlist-playlists | 21 +++ test/socket_recordings/initialversion | 2 + test/socket_recordings/listplaylist | 21 +++ test/socket_recordings/listplaylists | 19 ++ test/socket_recordings/queue | 65 +++++++ test/socket_spoof.rb | 152 +++++++++++++++ test/test_command_lists.rb | 48 +++++ 14 files changed, 563 insertions(+), 25 deletions(-) create mode 100644 lib/ruby-mpd/plugins/command_list.rb create mode 100644 test/record_sample_responses.rb create mode 100644 test/socket_recordings/commandlist-clear_addid create mode 100644 test/socket_recordings/commandlist-playlists create mode 100644 test/socket_recordings/initialversion create mode 100644 test/socket_recordings/listplaylist create mode 100644 test/socket_recordings/listplaylists create mode 100644 test/socket_recordings/queue create mode 100644 test/socket_spoof.rb create mode 100644 test/test_command_lists.rb diff --git a/lib/ruby-mpd.rb b/lib/ruby-mpd.rb index 446ccfb..0c6b546 100644 --- a/lib/ruby-mpd.rb +++ b/lib/ruby-mpd.rb @@ -1,22 +1,23 @@ require 'socket' require 'thread' -require 'ruby-mpd/version' -require 'ruby-mpd/exceptions' -require 'ruby-mpd/song' -require 'ruby-mpd/parser' -require 'ruby-mpd/playlist' - -require 'ruby-mpd/plugins/information' -require 'ruby-mpd/plugins/playback_options' -require 'ruby-mpd/plugins/controls' -require 'ruby-mpd/plugins/queue' -require 'ruby-mpd/plugins/playlists' -require 'ruby-mpd/plugins/database' -require 'ruby-mpd/plugins/stickers' -require 'ruby-mpd/plugins/outputs' -require 'ruby-mpd/plugins/reflection' -require 'ruby-mpd/plugins/channels' +require_relative 'ruby-mpd/version' +require_relative 'ruby-mpd/exceptions' +require_relative 'ruby-mpd/song' +require_relative 'ruby-mpd/parser' +require_relative 'ruby-mpd/playlist' + +require_relative 'ruby-mpd/plugins/information' +require_relative 'ruby-mpd/plugins/playback_options' +require_relative 'ruby-mpd/plugins/controls' +require_relative 'ruby-mpd/plugins/queue' +require_relative 'ruby-mpd/plugins/playlists' +require_relative 'ruby-mpd/plugins/database' +require_relative 'ruby-mpd/plugins/stickers' +require_relative 'ruby-mpd/plugins/outputs' +require_relative 'ruby-mpd/plugins/reflection' +require_relative 'ruby-mpd/plugins/channels' +require_relative 'ruby-mpd/plugins/command_list' # @!macro [new] error_raise # @raise (see #send_command) @@ -38,6 +39,7 @@ class MPD include Plugins::Outputs include Plugins::Reflection include Plugins::Channels + include Plugins::CommandList attr_reader :version, :hostname, :port diff --git a/lib/ruby-mpd/parser.rb b/lib/ruby-mpd/parser.rb index eea4b36..b4d3938 100644 --- a/lib/ruby-mpd/parser.rb +++ b/lib/ruby-mpd/parser.rb @@ -26,6 +26,8 @@ def convert_command(command, *params) end when MPD::Song quotable_param param.file + when MPD::Playlist + quotable_param param.name when Hash # normally a search query param.each_with_object("") do |(type, what), query| query << "#{type} #{quotable_param what} " @@ -59,7 +61,7 @@ def quotable_param(value) RETURN_ARRAY = Set[:channels, :outputs, :readmessages, :list, :listallinfo, :find, :search, :listplaylists, :listplaylist, :playlistfind, :playlistsearch, :plchanges, :tagtypes, :commands, :notcommands, :urlhandlers, - :decoders, :listplaylistinfo, :playlistinfo] + :decoders, :listplaylistinfo, :playlistinfo, :commandlist] # Parses key-value pairs into correct class. def parse_key(key, value) @@ -120,8 +122,8 @@ def build_hash(string) # Converts the response to MPD::Song objects. # @return [Array] An array of songs. - def build_songs_list(array) - return array.map { |hash| Song.new(self, hash) } + def build_songs_list(array=nil) + array.map { |hash| Song.new(self, hash) } if array end # Make chunks from string. @@ -154,10 +156,10 @@ def parse_response(command, string) # or an array of objects or an array of hashes). # # @return [Array, Array, String, Integer] Parsed response. - def build_response(command, string) + def build_response(command, string, force_hash=nil) chunks = make_chunks(string) # if there are any new lines (more than one data piece), it's a hash, else an object. - is_hash = chunks.any? { |chunk| chunk.include? "\n" } + is_hash = force_hash || chunks.any?{ |chunk| chunk.include? "\n" } list = chunks.inject([]) do |result, chunk| result << (is_hash ? build_hash(chunk) : parse_line(chunk)[1]) # parse_line(chunk)[1] is object diff --git a/lib/ruby-mpd/plugins/command_list.rb b/lib/ruby-mpd/plugins/command_list.rb new file mode 100644 index 0000000..a388fe2 --- /dev/null +++ b/lib/ruby-mpd/plugins/command_list.rb @@ -0,0 +1,177 @@ +module MPD::Plugins + + # Batch send multiple commands at once for speed. + module CommandList + # Send multiple commands at once. + # + # By default, any response from the server is ignored (for speed). + # To get results, pass +response_type+ as one of the following options: + # + # * +:hash+ — a single hash of return values; multiple return values for the same key are grouped in an array + # * +:values+ — an array of individual values + # * +:hashes+ — an array of hashes (where value boundaries are guessed based on the first result) + # * +:songs+ — an array of Song instances from the results + # * +:playlists+ - an array of Playlist instances from the results + # + # Note that each supported command has no return value inside the block. + # Instead, the block itself returns the array of results. + # + # @param [Symbol] response_type the type of responses desired. + # @return [nil] default behavior. + # @return [Array] if +response_type+ is +:values+, +:hashes+, +:songs+, or +:playlists+. + # @return [Hash] if +response_type+ is +:hash+. + # + # @example Simple batched control commands + # @mpd.command_list do + # stop + # shuffle + # save "shuffled" + # end + # + # @example Adding songs to the queue, ignoring the response + # @mpd.command_list do + # my_songs.each do |song| + # add(song) + # end + # end + # + # @example Adding songs to the queue and getting the song ids + # ids = @mpd.command_list(:values){ my_songs.each{ |song| addid(song) } } + # #=> [799,800,801,802,803] + # + # ids = @mpd.command_list(:hashes){ my_songs.each{ |song| addid(song) } } + # #=> [ {:id=>809}, {:id=>810}, {:id=>811}, {:id=>812}, {:id=>813} ] + # + # ids = @mpd.command_list(:hash){ my_songs.each{ |song| addid(song) } } + # #=> { :id=>[804,805,806,807,808] } + # + # @example Finding songs matching various genres + # songs = @mpd.command_list(:songs) do + # where genre:'Alternative Rock' + # where genre:'Alt. Rock' + # where genre:'alt-rock' + # end + # + # @see CommandList::Commands CommandList::Commands for a list of supported commands. + def command_list(response_type=nil,&commands) + @mutex.synchronize do + begin + socket.puts "command_list_begin" + CommandList::Commands.new(self).instance_eval(&commands) + socket.puts "command_list_end" + + # clear the response from the socket, even if we will not parse it + response = handle_server_response || "" + + case response_type + when :values then response.lines.map{ |line| parse_line(line).last } + when :hash then build_hash(response) + when :hashes then build_response(:commandlist,response,true) + when :songs then build_songs_list parse_response(:listallinfo,response) + when :playlists then parse_response(:listplaylists,response).map{ |h| MPD::Playlist.new(self, h) } + end + rescue Errno::EPIPE + reconnect + retry + end + end + end + end + + class CommandList::Commands + def initialize(mpd) + @mpd = mpd + end + + include MPD::Plugins::Controls + include MPD::Plugins::PlaybackOptions + include MPD::Plugins::Queue + include MPD::Plugins::Stickers + include MPD::Plugins::Database + + # List all of the playlists in the database + def playlists + send_command(:listplaylists) + end + + # Fetch full details for all songs in a playlist + # @param [String,Playlist] playlist The string name (or playlist object) to get songs for. + def songs_in_playlist(playlist) + send_command(:listplaylistinfo, playlist) + end + + # Fetch file names only for all songs in a playlist + # @param [String,Playlist] playlist The string name (or playlist object) to get files for. + def files_in_playlist(playlist) + send_command(:listplaylist, playlist) + end + + # Load the playlist's songs into the queue. + # + # Since MPD v0.17 a range can be passed to load only a part of the playlist. + # @param [String,Playlist] playlist The string name (or playlist object) to load songs from. + # @param [Range] range The index range of songs to add. + def load_playlist(playlist, range=nil) + send_command :load, playlist, range + end + + # Add a song to the playlist. + # @param [String,Playlist] playlist The string name (or playlist object) to add to. + # @param [String,Song] song The string uri (or song object) to add to the playlist + def add_to_playlist(playlist, song) + send_command :playlistadd, playlist, song + end + + # Search for any song that contains +value+ in the +tag+ field + # and add them to a playlist. + # Searches are *NOT* case sensitive. + # + # @param [String,Playlist] playlist The string name (or playlist object) to add to. + # @param [Symbol] tag Can be any tag supported by MPD, or one of the two special + # parameters: +:file+ to search by full path (relative to database root), + # and +:any+ to match against all available tags. + # @param [String] value The string to search for. + def searchadd_to_playlist(playlist, tag, value) + send_command :searchaddpl, playlist, tag, value + end + + # Remove all songs from a playlist. + # @param [String,Playlist] playlist The string name (or playlist object) to clear. + def clear_playlist(playlist) + send_command :playlistclear, playlist + end + + # Delete song at +index+ from a playlist. + # @param [String,Playlist] playlist The string name (or playlist object) to affect. + # @param [Integer] index The index of the song to remove. + def remove_from_playlist(playlist, index) + send_command :playlistdelete, playlist, index + end + + # Move a song with +song_id+ in a playlist to a new +index+. + # @param [String,Playlist] playlist The string name (or playlist object) to affect. + # @param [Integer] songid The +id+ of the song to move. + # @param [Integer] index The index to move the song to. + def reorder_playlist(playlist, song_id, index) + send_command :playlistmove, playlist, song_id, songpos + end + + # Rename a playlist to +new_name+. + # @param [String,Playlist] playlist The string name (or playlist object) to rename. + # @param [String] new_name The new name for the playlist. + def rename_playlist(playlist,new_name) + send_command :rename, playlist, new_name + end + + # Delete a playlist from the disk. + # @param [String,Playlist] playlist The string name (or playlist object) to delete. + def destroy_playlist(playlist) + send_command :rm, playlist + end + + private + def send_command(command,*args) + @mpd.send(:socket).puts @mpd.send(:convert_command, command, *args) + end + end +end \ No newline at end of file diff --git a/lib/ruby-mpd/plugins/database.rb b/lib/ruby-mpd/plugins/database.rb index 9f32571..615c270 100644 --- a/lib/ruby-mpd/plugins/database.rb +++ b/lib/ruby-mpd/plugins/database.rb @@ -93,8 +93,9 @@ def where(params, options = {}) end response = send_command(command, params) - if response == true - return true + case response + when true then true + when nil then nil else build_songs_list response end diff --git a/lib/ruby-mpd/plugins/stickers.rb b/lib/ruby-mpd/plugins/stickers.rb index f20665f..aad1196 100644 --- a/lib/ruby-mpd/plugins/stickers.rb +++ b/lib/ruby-mpd/plugins/stickers.rb @@ -40,8 +40,9 @@ def delete_sticker(type, uri, name = nil) # @return [Hash] Hash mapping sticker names (as strings) to values (as strings). def list_stickers(type, uri) result = send_command :sticker, :list, type, uri - if result==true # response when there are no - {} + case result + when true then {} # response when there are no stickers + when nil then nil # when called in a command_list else result = [result] if result.is_a?(String) Hash[result.map{|s| s.split('=',2)}] diff --git a/test/record_sample_responses.rb b/test/record_sample_responses.rb new file mode 100644 index 0000000..3f30cb6 --- /dev/null +++ b/test/record_sample_responses.rb @@ -0,0 +1,16 @@ +require '../lib/ruby-mpd' +require './socket_spoof' + +class RecordingMPD < MPD + def socket + @recording_socket ||= SocketSpoof::Recorder.new(super) + end +end + +m = RecordingMPD.new('music.local').tap(&:connect) +begin + s = ["gavin/Basic Pleasure Model/How to Live/01 How to Live (Album Version).m4a","gavin/Basic Pleasure Model/Sunyata/01 Sunyata (album Version).m4a"] + m.command_list{ s.each{ |f| addid(f) } } + m.queue + m.playlists.find{ |pl| pl.name=='user-gkistner' }.songs +end \ No newline at end of file diff --git a/test/socket_recordings/commandlist-clear_addid b/test/socket_recordings/commandlist-clear_addid new file mode 100644 index 0000000..1d02472 --- /dev/null +++ b/test/socket_recordings/commandlist-clear_addid @@ -0,0 +1,11 @@ +command_list_begin +clear +addid test1.mp3 +addid "test 2.mp3" +addid test3.mp3 +command_list_end +--putsabove--getsbelow-- +Id: 107 +Id: 108 +Id: 109 +OK diff --git a/test/socket_recordings/commandlist-playlists b/test/socket_recordings/commandlist-playlists new file mode 100644 index 0000000..93eef5e --- /dev/null +++ b/test/socket_recordings/commandlist-playlists @@ -0,0 +1,21 @@ +command_list_begin +listplaylists +command_list_end +--putsabove--getsbelow-- +playlist: Mix Rock Alternative Electric +Last-Modified: 2015-11-23T15:58:51Z +playlist: SNBRN +Last-Modified: 2016-01-26T00:25:52Z +playlist: Enya-esque +Last-Modified: 2015-11-18T16:19:12Z +playlist: RecentNice +Last-Modified: 2015-12-01T15:52:38Z +playlist: Dancetown +Last-Modified: 2015-11-18T16:19:26Z +playlist: Piano +Last-Modified: 2015-11-18T16:17:13Z +playlist: Thump +Last-Modified: 2015-11-20T15:32:30Z +playlist: GavinLikesIt +Last-Modified: 2015-11-20T15:54:49Z +OK diff --git a/test/socket_recordings/initialversion b/test/socket_recordings/initialversion new file mode 100644 index 0000000..b373dbb --- /dev/null +++ b/test/socket_recordings/initialversion @@ -0,0 +1,2 @@ +--putsabove--getsbelow-- +OK MPD 0.19.0 diff --git a/test/socket_recordings/listplaylist b/test/socket_recordings/listplaylist new file mode 100644 index 0000000..f2fd2ac --- /dev/null +++ b/test/socket_recordings/listplaylist @@ -0,0 +1,21 @@ +listplaylistinfo user-gkistner +--putsabove--getsbelow-- +file: gavin/Crash Test Dummies/The Ghosts that Haunt Me/06 The Ghosts That Haunt Me.mp3 +Last-Modified: 2016-02-03T18:28:40Z +Artist: Crash Test Dummies +Title: The Ghosts That Haunt Me +Album: The Ghosts that Haunt Me +Track: 6/10 +Date: 1991 +Genre: Rock +Time: 226 +file: gavin/Crash Test Dummies/The Ghosts that Haunt Me/04 The Country Life.mp3 +Last-Modified: 2016-02-03T18:28:40Z +Artist: Crash Test Dummies +Title: The Country Life +Album: The Ghosts that Haunt Me +Track: 4/10 +Date: 1991 +Genre: Rock +Time: 243 +OK diff --git a/test/socket_recordings/listplaylists b/test/socket_recordings/listplaylists new file mode 100644 index 0000000..a8698d0 --- /dev/null +++ b/test/socket_recordings/listplaylists @@ -0,0 +1,19 @@ +listplaylists +--putsabove--getsbelow-- +playlist: Mix Rock Alternative Electric +Last-Modified: 2015-11-23T15:58:51Z +playlist: SNBRN +Last-Modified: 2016-01-26T00:25:52Z +playlist: Enya-esque +Last-Modified: 2015-11-18T16:19:12Z +playlist: RecentNice +Last-Modified: 2015-12-01T15:52:38Z +playlist: Dancetown +Last-Modified: 2015-11-18T16:19:26Z +playlist: Piano +Last-Modified: 2015-11-18T16:17:13Z +playlist: Thump +Last-Modified: 2015-11-20T15:32:30Z +playlist: GavinLikesIt +Last-Modified: 2015-11-20T15:54:49Z +OK diff --git a/test/socket_recordings/queue b/test/socket_recordings/queue new file mode 100644 index 0000000..b91730c --- /dev/null +++ b/test/socket_recordings/queue @@ -0,0 +1,65 @@ +playlistinfo +--putsabove--getsbelow-- +file: gavin/Crash Test Dummies/God Shuffled His Feet/01 God Shuffled His Feet 1.mp3 +Last-Modified: 2004-03-13T03:33:24Z +Artist: Crash Test Dummies +Title: God Shuffled His Feet +Album: God Shuffled His Feet +Track: 1/12 +Date: 1993 +Genre: Alternative & Punk +Time: 310 +Pos: 0 +Id: 1295 +Prio: 2 +file: gavin/Crash Test Dummies/The Ghosts that Haunt Me/06 The Ghosts That Haunt Me.mp3 +Last-Modified: 2016-02-03T18:28:40Z +Artist: Crash Test Dummies +Title: The Ghosts That Haunt Me +Album: The Ghosts that Haunt Me +Track: 6/10 +Date: 1991 +Genre: Rock +Time: 226 +Pos: 1 +Id: 1298 +Prio: 2 +file: gavin/Crash Test Dummies/The Ghosts that Haunt Me/04 The Country Life.mp3 +Last-Modified: 2016-02-03T18:28:40Z +Artist: Crash Test Dummies +Title: The Country Life +Album: The Ghosts that Haunt Me +Track: 4/10 +Date: 1991 +Genre: Rock +Time: 243 +Pos: 2 +Id: 1299 +Prio: 2 +file: gavin/Basic Pleasure Model/How to Live/01 How to Live (Album Version).m4a +Last-Modified: 2016-02-17T15:49:02Z +Artist: Basic Pleasure Model +Album: How to Live +Title: How to Live (Album Version) +Track: 1/12 +Genre: Electronic +Date: 2004-04-22T07:00:00Z +Disc: 1/1 +AlbumArtist: Basic Pleasure Model +Time: 258 +Pos: 3 +Id: 1300 +file: gavin/Basic Pleasure Model/Sunyata/01 Sunyata (album Version).m4a +Last-Modified: 2016-02-17T15:49:03Z +Artist: Basic Pleasure Model +Album: Sunyata +Title: Sunyata (album Version) +Track: 1/8 +Genre: Electronic +Date: 2003-12-18T08:00:00Z +Disc: 1/1 +AlbumArtist: Basic Pleasure Model +Time: 347 +Pos: 4 +Id: 1301 +OK diff --git a/test/socket_spoof.rb b/test/socket_spoof.rb new file mode 100644 index 0000000..d8a9e01 --- /dev/null +++ b/test/socket_spoof.rb @@ -0,0 +1,152 @@ +require 'fileutils' # used for mkdir_p +require 'digest' # used to generate unique file names + +# A library for testing socket-based applications. +# +# Allows you to create a socket that records +puts+ commands +# and uses those to decide the (pre-recorded) responses to +# yield for subsequent calls to +gets+. +module SocketSpoof + # The line in each recording file separating commands and responses + SPLITTER = "--putsabove--getsbelow--\n" + + # Socket wrapper that generates 'recording' files consumed + # by SocketSpoof::Player. + # + # To use, replace your own socket with a call to: + # + # @socket = SocketSpoof::Recorder.new( real_socket ) + # + # This will (by default) create a directory named "socket_recordings" + # and create files within there for each sequence of +puts+ followed + # by one or more gets. + class Recorder + # @param socket [Socket] The real socket to use for communication. + # @param directory [String] The directory to store recording files in. + def initialize(socket,directory:"socket_recordings") + @socket = socket + @commands = [] + FileUtils.mkdir_p( @directory=directory ) + end + def puts(*a) + @socket.puts(*a).tap{ @commands.concat(a.empty? ? [nil] : a) } + end + def gets + @socket.gets.tap do |response| + unless @file && @commands.empty? + @file = File.join( @directory, Digest::SHA256.hexdigest(@commands.inspect) ) + File.open(@file,'w'){ |f| f.puts(@commands); f<] messages previously sent through +puts+ + def last_messages + @current + end + def puts(*a) + @commands.concat(a.empty? ? [nil] : a) + @response_line = -1 + nil # match the return value of IO#puts, just in case + end + def gets + rescan if @auto_update + @current,@commands=@commands,[] unless @commands.empty? + if @responses[@current] + @responses[@current][@response_line+=1] + else + raise "#{self.class} has no recording for #{@current}" + end + end + def method_missing(*a) + raise "#{self.class} has no support for #{a.shift}(#{a.map(&:inspect).join(', ')})" + end + + private + def rescan + @responses = {} + Dir[File.join(@directory,'*')].each do |file| + commands,responses = File.open(file,'r:utf-8',&:read).split(SPLITTER,2) + if responses + @responses[commands.split("\n")] = responses.lines.to_a + else + warn "#{self.class} ignoring #{file} because it does not appear to have #{SPLITTER.inspect}." + end + end + end + end +end \ No newline at end of file diff --git a/test/test_command_lists.rb b/test/test_command_lists.rb new file mode 100644 index 0000000..562271f --- /dev/null +++ b/test/test_command_lists.rb @@ -0,0 +1,48 @@ +require '../lib/ruby-mpd' +require './socket_spoof' + +class PlaybackMPD < MPD + def initialize( recordings_directory=nil ) + super() + @socket = SocketSpoof::Player.new(directory:recordings_directory) + end + def last_messages + @socket.last_messages + end +end + +require 'minitest/autorun' +class TestQueue < MiniTest::Unit::TestCase + def setup + @mpd = PlaybackMPD.new 'socket_recordings' + end + + def test_songs + songs = @mpd.queue + assert_equal ["playlistinfo"], @mpd.last_messages + assert_equal 5, songs.length + assert songs.all?{ |value| value.is_a? MPD::Song } + assert_equal [310,226,243,258,347], songs.map(&:track_length) + end + + def test_command_lists + ids = @mpd.command_list(:values) do + clear + %w[test1.mp3 test\ 2.mp3 test3.mp3].each{ |f| addid(f) } + end + assert_equal( + ["command_list_begin", "clear", "addid test1.mp3", 'addid "test 2.mp3"', + "addid test3.mp3", "command_list_end"], + @mpd.last_messages + ) + assert_equal [107,108,109], ids + + pls = @mpd.command_list(:playlists){ playlists } + assert_equal( + ["command_list_begin", "listplaylists", "command_list_end"], + @mpd.last_messages + ) + assert_equal(8,pls.length) + assert pls.all?{ |value| value.is_a? MPD::Playlist } + end +end \ No newline at end of file From 2968621250a069745f4ada676f88bf40e44ff371 Mon Sep 17 00:00:00 2001 From: Gavin Kistner Date: Wed, 17 Feb 2016 22:36:31 -0700 Subject: [PATCH 2/9] Add more command_list tests (and fix bugs found in the process) --- lib/ruby-mpd/parser.rb | 19 +++- lib/ruby-mpd/plugins/command_list.rb | 2 +- lib/ruby-mpd/version.rb | 2 +- test/record_sample_responses.rb | 5 +- test/test_command_lists.rb | 160 +++++++++++++++++++++++++-- 5 files changed, 168 insertions(+), 20 deletions(-) diff --git a/lib/ruby-mpd/parser.rb b/lib/ruby-mpd/parser.rb index b4d3938..190bef2 100644 --- a/lib/ruby-mpd/parser.rb +++ b/lib/ruby-mpd/parser.rb @@ -107,13 +107,20 @@ def parse_line(line) # The end result is a hash containing the proper key/value pairs def build_hash(string) return {} if string.nil? + array_keys = {} string.lines.each_with_object({}) do |line, hash| key, object = parse_line(line) # if val appears more than once, make an array of vals. if hash.include? key - hash[key] = Array(hash[key]) << object + # cannot use Array(hash[key]) or [*hash[key]] because Time instances get splatted + # cannot check for is_a?(Array) because some values (time) are already arrays + unless array_keys[key] + hash[key] = [hash[key]] + array_keys[key] = true + end + hash[key] << object else # val hasn't appeared yet, map it. hash[key] = object # map obj to key end @@ -123,7 +130,15 @@ def build_hash(string) # Converts the response to MPD::Song objects. # @return [Array] An array of songs. def build_songs_list(array=nil) - array.map { |hash| Song.new(self, hash) } if array + array.map{ |hash| hash.is_a?(Hash) ? Song.new(self, hash) : nil }.compact if array + end + + # Converts the response to MPD::Playlist objects. + # @return [Array] An array of playlists. + def build_playlists(array) + array.map do |hash| + Playlist.new(self, hash) if hash.is_a?(Hash) && hash[:playlist] + end.compact end # Make chunks from string. diff --git a/lib/ruby-mpd/plugins/command_list.rb b/lib/ruby-mpd/plugins/command_list.rb index a388fe2..bd90c47 100644 --- a/lib/ruby-mpd/plugins/command_list.rb +++ b/lib/ruby-mpd/plugins/command_list.rb @@ -68,7 +68,7 @@ def command_list(response_type=nil,&commands) when :hash then build_hash(response) when :hashes then build_response(:commandlist,response,true) when :songs then build_songs_list parse_response(:listallinfo,response) - when :playlists then parse_response(:listplaylists,response).map{ |h| MPD::Playlist.new(self, h) } + when :playlists then build_playlists parse_response(:listplaylists,response) end rescue Errno::EPIPE reconnect diff --git a/lib/ruby-mpd/version.rb b/lib/ruby-mpd/version.rb index 8108e07..3004347 100644 --- a/lib/ruby-mpd/version.rb +++ b/lib/ruby-mpd/version.rb @@ -1,3 +1,3 @@ class MPD - VERSION = '0.3.4' + VERSION = '0.4.0' end diff --git a/test/record_sample_responses.rb b/test/record_sample_responses.rb index 3f30cb6..74799af 100644 --- a/test/record_sample_responses.rb +++ b/test/record_sample_responses.rb @@ -7,10 +7,7 @@ def socket end end -m = RecordingMPD.new('music.local').tap(&:connect) +m = RecordingMPD.new.tap(&:connect) begin - s = ["gavin/Basic Pleasure Model/How to Live/01 How to Live (Album Version).m4a","gavin/Basic Pleasure Model/Sunyata/01 Sunyata (album Version).m4a"] m.command_list{ s.each{ |f| addid(f) } } - m.queue - m.playlists.find{ |pl| pl.name=='user-gkistner' }.songs end \ No newline at end of file diff --git a/test/test_command_lists.rb b/test/test_command_lists.rb index 562271f..14570fb 100644 --- a/test/test_command_lists.rb +++ b/test/test_command_lists.rb @@ -25,24 +25,160 @@ def test_songs assert_equal [310,226,243,258,347], songs.map(&:track_length) end - def test_command_lists - ids = @mpd.command_list(:values) do + def test_command_list_ids + clear_and_add = proc{ clear %w[test1.mp3 test\ 2.mp3 test3.mp3].each{ |f| addid(f) } - end + } + + result = @mpd.command_list(&clear_and_add) + assert_nil result + assert_equal( + ['command_list_begin','clear','addid test1.mp3','addid "test 2.mp3"','addid test3.mp3','command_list_end'], + @mpd.last_messages, + "command list must send commands even if no result is desired" + ) + + ids = @mpd.command_list(:hash,&clear_and_add) + assert_equal({id:[107,108,109]}, ids) + + ids = @mpd.command_list(:hashes,&clear_and_add) + assert_equal([{id:107},{id:108},{id:109}], ids) + + ids = @mpd.command_list(:values,&clear_and_add) + assert_equal([107,108,109], ids) + + songs = @mpd.command_list(:songs,&clear_and_add) + assert_equal([], songs, "no songs should be created from invalid data") + + lists = @mpd.command_list(:playlists,&clear_and_add) + assert_equal([], lists, "no playlists should be created from invalid data") + end + + def test_command_list_playlists + assert_nil @mpd.command_list{ playlists } + assert_equal( + %w[command_list_begin listplaylists command_list_end], + @mpd.last_messages, + "command list must send commands even if no result is desired" + ) + + assert_equal( + { + playlist:["Mix Rock Alternative Electric", "SNBRN", "Enya-esque", "RecentNice", "Dancetown", "Piano", "Thump", "GavinLikesIt"], + :"last-modified" => [ + Time.iso8601('2015-11-23T15:58:51Z'), Time.iso8601('2016-01-26T00:25:52Z'), Time.iso8601('2015-11-18T16:19:12Z'), + Time.iso8601('2015-12-01T15:52:38Z'), Time.iso8601('2015-11-18T16:19:26Z'), Time.iso8601('2015-11-18T16:17:13Z'), + Time.iso8601('2015-11-20T15:32:30Z'), Time.iso8601('2015-11-20T15:54:49Z'), + ] + }, + @mpd.command_list(:hash){ playlists } + ) + assert_equal( - ["command_list_begin", "clear", "addid test1.mp3", 'addid "test 2.mp3"', - "addid test3.mp3", "command_list_end"], - @mpd.last_messages + [ + { playlist: "Mix Rock Alternative Electric", :"last-modified" => Time.iso8601('2015-11-23T15:58:51Z') }, + { playlist: "SNBRN", :"last-modified" => Time.iso8601('2016-01-26T00:25:52Z') }, + { playlist: "Enya-esque", :"last-modified" => Time.iso8601('2015-11-18T16:19:12Z') }, + { playlist: "RecentNice", :"last-modified" => Time.iso8601('2015-12-01T15:52:38Z') }, + { playlist: "Dancetown", :"last-modified" => Time.iso8601('2015-11-18T16:19:26Z') }, + { playlist: "Piano", :"last-modified" => Time.iso8601('2015-11-18T16:17:13Z') }, + { playlist: "Thump", :"last-modified" => Time.iso8601('2015-11-20T15:32:30Z') }, + { playlist: "GavinLikesIt", :"last-modified" => Time.iso8601('2015-11-20T15:54:49Z') }, + ], + @mpd.command_list(:hashes){ playlists } ) - assert_equal [107,108,109], ids - pls = @mpd.command_list(:playlists){ playlists } assert_equal( - ["command_list_begin", "listplaylists", "command_list_end"], - @mpd.last_messages + [ + "Mix Rock Alternative Electric", Time.iso8601('2015-11-23T15:58:51Z'), + "SNBRN", Time.iso8601('2016-01-26T00:25:52Z'), + "Enya-esque", Time.iso8601('2015-11-18T16:19:12Z'), + "RecentNice", Time.iso8601('2015-12-01T15:52:38Z'), + "Dancetown", Time.iso8601('2015-11-18T16:19:26Z'), + "Piano", Time.iso8601('2015-11-18T16:17:13Z'), + "Thump", Time.iso8601('2015-11-20T15:32:30Z'), + "GavinLikesIt", Time.iso8601('2015-11-20T15:54:49Z'), + ], + @mpd.command_list(:values){ playlists } ) - assert_equal(8,pls.length) - assert pls.all?{ |value| value.is_a? MPD::Playlist } + + assert_equal( [], @mpd.command_list(:songs){ playlists } ) + + lists = @mpd.command_list(:playlists){ playlists } + assert_equal(8,lists.length) + lists.each{ |value| assert_kind_of MPD::Playlist, value, ":playlists should only return playlists" } + assert lists.any?{ |list| list.name=="Thump" }, "one of the playlists should be named 'Thump'" end + + def test_command_list_songs + twogenres = proc{ where(genre:'alt'); where(genre:'trance') } + assert_nil @mpd.command_list(&twogenres) + assert_equal( + ['command_list_begin','search genre alt','search genre trance','command_list_end'], + @mpd.last_messages, + "command list must send commands even if no result is desired" + ) + + result = @mpd.command_list(:hash,&twogenres) + assert_kind_of Hash, result, ":hash style always returns a hash" + assert_equal 12, result.size, "there are 12 distinct value types in this song set" + result.values.each do |v| + assert_kind_of Array, v, "all hash keys are arrays" + assert_equal 5, v.length, "there are 5 values for each hash key" + end + assert_equal [2,5,2,2,2], result[:track] + + result = @mpd.command_list(:hashes,&twogenres) + assert_kind_of Array, result, ":hashes style always returns an array" + assert_equal 5, result.size, "there are 5 hash clumps returned" + result.each{ |h| assert_equal 12, h.length, "every hash should have 12 values" } + + result = @mpd.command_list(:values,&twogenres) + assert_kind_of Array, result, ":values style always returns an array" + assert_equal 60, result.size, "there are 60 individual values in the result set" + + result = @mpd.command_list(:songs,&twogenres) + assert_kind_of Array, result, ":songs style always returns an array" + assert_equal 5, result.size, "there are 5 songs in the result set" + result.each{ |v| assert_kind_of MPD::Song, v, "all results are songs" } + + result = @mpd.command_list(:playlists,&twogenres) + assert_kind_of Array, result, ":playlists style always returns an array" + assert_empty result, "there are no playlists in the result set" + end + + def test_command_list_song + onegenre = proc{ where genre:'trance' } + assert_nil @mpd.command_list(&onegenre) + assert_equal( + ['command_list_begin','search genre trance','command_list_end'], + @mpd.last_messages, + "command list must send commands even if no result is desired" + ) + + result = @mpd.command_list(:hash,&onegenre) + assert_kind_of Hash, result, ":hash style always returns a hash" + assert_equal 12, result.size, "there are 12 distinct value types in this song set" + assert_equal "Morphine", result[:title] + + result = @mpd.command_list(:hashes,&onegenre) + assert_kind_of Array, result, ":hashes style always returns an array" + assert_equal 1, result.size, "there is one hash returned" + result.each{ |h| assert_equal 12, h.length, "every hash should have 12 values" } + + result = @mpd.command_list(:values,&onegenre) + assert_kind_of Array, result, ":values style always returns an array" + assert_equal 12, result.size, "there are 12 individual values in the result set" + + result = @mpd.command_list(:songs,&onegenre) + assert_kind_of Array, result, ":songs style always returns an array" + assert_equal 1, result.size, "there is 1 song in the result set" + result.each{ |v| assert_kind_of MPD::Song, v, "all results are songs" } + + result = @mpd.command_list(:playlists,&onegenre) + assert_kind_of Array, result, ":playlists style always returns an array" + assert_empty result, "there are no playlists in the result set" + end + end \ No newline at end of file From bc8a28d442b8f58517533c4c56866e06849da137 Mon Sep 17 00:00:00 2001 From: Gavin Kistner Date: Thu, 18 Feb 2016 10:49:31 -0700 Subject: [PATCH 3/9] Clean test data to be less personal --- test/socket_recordings/commandlist-playlists | 4 +- .../socket_recordings/commandlist-where-multi | 65 +++++++++++++++++++ .../commandlist-where-single | 16 +++++ test/socket_recordings/listplaylist | 4 +- test/socket_recordings/listplaylists | 4 +- test/socket_recordings/queue | 10 +-- test/test_command_lists.rb | 14 ++-- 7 files changed, 99 insertions(+), 18 deletions(-) create mode 100644 test/socket_recordings/commandlist-where-multi create mode 100644 test/socket_recordings/commandlist-where-single diff --git a/test/socket_recordings/commandlist-playlists b/test/socket_recordings/commandlist-playlists index 93eef5e..d9a7693 100644 --- a/test/socket_recordings/commandlist-playlists +++ b/test/socket_recordings/commandlist-playlists @@ -2,7 +2,7 @@ command_list_begin listplaylists command_list_end --putsabove--getsbelow-- -playlist: Mix Rock Alternative Electric +playlist: Mix Rock Alt Electric Last-Modified: 2015-11-23T15:58:51Z playlist: SNBRN Last-Modified: 2016-01-26T00:25:52Z @@ -16,6 +16,6 @@ playlist: Piano Last-Modified: 2015-11-18T16:17:13Z playlist: Thump Last-Modified: 2015-11-20T15:32:30Z -playlist: GavinLikesIt +playlist: Smooth Town Last-Modified: 2015-11-20T15:54:49Z OK diff --git a/test/socket_recordings/commandlist-where-multi b/test/socket_recordings/commandlist-where-multi new file mode 100644 index 0000000..3e9f01a --- /dev/null +++ b/test/socket_recordings/commandlist-where-multi @@ -0,0 +1,65 @@ +command_list_begin +search genre alt +search genre trance +command_list_end +--putsabove--getsbelow-- +file: Jane's Addiction/Nothing's Shocking/05 Standing In The Shower... Thinking.mp3 +Last-Modified: 2005-08-28T19:36:48Z +Time: 185 +Artist: Jane's Addiction +AlbumArtist: Jane's Addiction +Title: Standing In The Shower... Thinking +Album: Nothing's Shocking +Track: 5 +Date: 1988 +Genre: Alternative & Punk +Composer: Jane's Addiction +Disc: 1/1 +file: doza/Goldfrapp/We Are Glitter/06 Number 1 (Alan Braxe & Fred Falke.mp3 +Last-Modified: 2009-11-23T03:48:58Z +Time: 440 +Artist: Goldfrapp +AlbumArtist: Goldfrapp +Title: Number 1 (Alan Braxe & Fred Falke Main Remix) +Album: We Are Glitter +Track: 6/12 +Date: 2006 +Genre: Alternative Rock +Composer: Goldfrapp +Disc: 1/1 +file: X Ambassadors/VHS/02 Renegades.m4a +Last-Modified: 2015-11-08T01:00:15Z +Time: 195 +Artist: X Ambassadors +Album: VHS +Title: Renegades +Track: 2/20 +Genre: Alternative +Date: 2015-06-30T07:00:00Z +Composer: Alex Da Kid, Sam Harris, Noah Feldshuh, Casey Harris & Adam Levin +Disc: 1/1 +AlbumArtist: X Ambassadors +file: gavin/Vampire Weekend/Contra (Bonus Track Version)/07 Cousins.m4a +Last-Modified: 2014-07-25T04:03:50Z +Time: 145 +Artist: Vampire Weekend +Album: Contra (Bonus Track Version) +Title: Cousins +Track: 7/11 +Genre: Alternative +Date: 2010-01-11T08:00:00Z +Composer: Ezra Koenig, Rostam Batmanglij, Chris Baio & Chris Tomson +Disc: 1/1 +AlbumArtist: Vampire Weekend +file: Thomas Gold/Morphine - Single/02 Morphine.m4a +Last-Modified: 2015-12-10T16:15:14Z +Time: 285 +Artist: Thomas Gold & Lush & Simon +Album: Morphine - Single +Title: Morphine +Track: 2/2 +Genre: Trance +Date: 2015-11-16T08:00:00Z +Composer: A. Miselli, F. Knebel-Janssen & S. Privitera +Disc: 1/1 +AlbumArtist: Thomas Gold diff --git a/test/socket_recordings/commandlist-where-single b/test/socket_recordings/commandlist-where-single new file mode 100644 index 0000000..f064b02 --- /dev/null +++ b/test/socket_recordings/commandlist-where-single @@ -0,0 +1,16 @@ +command_list_begin +search genre trance +command_list_end +--putsabove--getsbelow-- +file: Thomas Gold/Morphine - Single/02 Morphine.m4a +Last-Modified: 2015-12-10T16:15:14Z +Time: 285 +Artist: Thomas Gold & Lush & Simon +Album: Morphine - Single +Title: Morphine +Track: 2/2 +Genre: Trance +Date: 2015-11-16T08:00:00Z +Composer: A. Miselli, F. Knebel-Janssen & S. Privitera +Disc: 1/1 +AlbumArtist: Thomas Gold diff --git a/test/socket_recordings/listplaylist b/test/socket_recordings/listplaylist index f2fd2ac..3cce17f 100644 --- a/test/socket_recordings/listplaylist +++ b/test/socket_recordings/listplaylist @@ -1,6 +1,6 @@ listplaylistinfo user-gkistner --putsabove--getsbelow-- -file: gavin/Crash Test Dummies/The Ghosts that Haunt Me/06 The Ghosts That Haunt Me.mp3 +file: Crash Test Dummies/The Ghosts that Haunt Me/06 The Ghosts That Haunt Me.mp3 Last-Modified: 2016-02-03T18:28:40Z Artist: Crash Test Dummies Title: The Ghosts That Haunt Me @@ -9,7 +9,7 @@ Track: 6/10 Date: 1991 Genre: Rock Time: 226 -file: gavin/Crash Test Dummies/The Ghosts that Haunt Me/04 The Country Life.mp3 +file: Crash Test Dummies/The Ghosts that Haunt Me/04 The Country Life.mp3 Last-Modified: 2016-02-03T18:28:40Z Artist: Crash Test Dummies Title: The Country Life diff --git a/test/socket_recordings/listplaylists b/test/socket_recordings/listplaylists index a8698d0..308dd3a 100644 --- a/test/socket_recordings/listplaylists +++ b/test/socket_recordings/listplaylists @@ -1,6 +1,6 @@ listplaylists --putsabove--getsbelow-- -playlist: Mix Rock Alternative Electric +playlist: Mix Rock Alt Electric Last-Modified: 2015-11-23T15:58:51Z playlist: SNBRN Last-Modified: 2016-01-26T00:25:52Z @@ -14,6 +14,6 @@ playlist: Piano Last-Modified: 2015-11-18T16:17:13Z playlist: Thump Last-Modified: 2015-11-20T15:32:30Z -playlist: GavinLikesIt +playlist: Smooth Town Last-Modified: 2015-11-20T15:54:49Z OK diff --git a/test/socket_recordings/queue b/test/socket_recordings/queue index b91730c..7199188 100644 --- a/test/socket_recordings/queue +++ b/test/socket_recordings/queue @@ -1,6 +1,6 @@ playlistinfo --putsabove--getsbelow-- -file: gavin/Crash Test Dummies/God Shuffled His Feet/01 God Shuffled His Feet 1.mp3 +file: Crash Test Dummies/God Shuffled His Feet/01 God Shuffled His Feet 1.mp3 Last-Modified: 2004-03-13T03:33:24Z Artist: Crash Test Dummies Title: God Shuffled His Feet @@ -12,7 +12,7 @@ Time: 310 Pos: 0 Id: 1295 Prio: 2 -file: gavin/Crash Test Dummies/The Ghosts that Haunt Me/06 The Ghosts That Haunt Me.mp3 +file: Crash Test Dummies/The Ghosts that Haunt Me/06 The Ghosts That Haunt Me.mp3 Last-Modified: 2016-02-03T18:28:40Z Artist: Crash Test Dummies Title: The Ghosts That Haunt Me @@ -24,7 +24,7 @@ Time: 226 Pos: 1 Id: 1298 Prio: 2 -file: gavin/Crash Test Dummies/The Ghosts that Haunt Me/04 The Country Life.mp3 +file: Crash Test Dummies/The Ghosts that Haunt Me/04 The Country Life.mp3 Last-Modified: 2016-02-03T18:28:40Z Artist: Crash Test Dummies Title: The Country Life @@ -36,7 +36,7 @@ Time: 243 Pos: 2 Id: 1299 Prio: 2 -file: gavin/Basic Pleasure Model/How to Live/01 How to Live (Album Version).m4a +file: Basic Pleasure Model/How to Live/01 How to Live (Album Version).m4a Last-Modified: 2016-02-17T15:49:02Z Artist: Basic Pleasure Model Album: How to Live @@ -49,7 +49,7 @@ AlbumArtist: Basic Pleasure Model Time: 258 Pos: 3 Id: 1300 -file: gavin/Basic Pleasure Model/Sunyata/01 Sunyata (album Version).m4a +file: Basic Pleasure Model/Sunyata/01 Sunyata (album Version).m4a Last-Modified: 2016-02-17T15:49:03Z Artist: Basic Pleasure Model Album: Sunyata diff --git a/test/test_command_lists.rb b/test/test_command_lists.rb index 14570fb..060680c 100644 --- a/test/test_command_lists.rb +++ b/test/test_command_lists.rb @@ -1,5 +1,6 @@ require '../lib/ruby-mpd' require './socket_spoof' +require 'minitest/autorun' class PlaybackMPD < MPD def initialize( recordings_directory=nil ) @@ -11,7 +12,6 @@ def last_messages end end -require 'minitest/autorun' class TestQueue < MiniTest::Unit::TestCase def setup @mpd = PlaybackMPD.new 'socket_recordings' @@ -65,7 +65,7 @@ def test_command_list_playlists assert_equal( { - playlist:["Mix Rock Alternative Electric", "SNBRN", "Enya-esque", "RecentNice", "Dancetown", "Piano", "Thump", "GavinLikesIt"], + playlist:["Mix Rock Alt Electric", "SNBRN", "Enya-esque", "RecentNice", "Dancetown", "Piano", "Thump", "Smooth Town"], :"last-modified" => [ Time.iso8601('2015-11-23T15:58:51Z'), Time.iso8601('2016-01-26T00:25:52Z'), Time.iso8601('2015-11-18T16:19:12Z'), Time.iso8601('2015-12-01T15:52:38Z'), Time.iso8601('2015-11-18T16:19:26Z'), Time.iso8601('2015-11-18T16:17:13Z'), @@ -77,28 +77,28 @@ def test_command_list_playlists assert_equal( [ - { playlist: "Mix Rock Alternative Electric", :"last-modified" => Time.iso8601('2015-11-23T15:58:51Z') }, + { playlist: "Mix Rock Alt Electric", :"last-modified" => Time.iso8601('2015-11-23T15:58:51Z') }, { playlist: "SNBRN", :"last-modified" => Time.iso8601('2016-01-26T00:25:52Z') }, { playlist: "Enya-esque", :"last-modified" => Time.iso8601('2015-11-18T16:19:12Z') }, { playlist: "RecentNice", :"last-modified" => Time.iso8601('2015-12-01T15:52:38Z') }, { playlist: "Dancetown", :"last-modified" => Time.iso8601('2015-11-18T16:19:26Z') }, { playlist: "Piano", :"last-modified" => Time.iso8601('2015-11-18T16:17:13Z') }, { playlist: "Thump", :"last-modified" => Time.iso8601('2015-11-20T15:32:30Z') }, - { playlist: "GavinLikesIt", :"last-modified" => Time.iso8601('2015-11-20T15:54:49Z') }, + { playlist: "Smooth Town", :"last-modified" => Time.iso8601('2015-11-20T15:54:49Z') }, ], @mpd.command_list(:hashes){ playlists } ) assert_equal( [ - "Mix Rock Alternative Electric", Time.iso8601('2015-11-23T15:58:51Z'), + "Mix Rock Alt Electric", Time.iso8601('2015-11-23T15:58:51Z'), "SNBRN", Time.iso8601('2016-01-26T00:25:52Z'), "Enya-esque", Time.iso8601('2015-11-18T16:19:12Z'), "RecentNice", Time.iso8601('2015-12-01T15:52:38Z'), "Dancetown", Time.iso8601('2015-11-18T16:19:26Z'), "Piano", Time.iso8601('2015-11-18T16:17:13Z'), "Thump", Time.iso8601('2015-11-20T15:32:30Z'), - "GavinLikesIt", Time.iso8601('2015-11-20T15:54:49Z'), + "Smooth Town", Time.iso8601('2015-11-20T15:54:49Z'), ], @mpd.command_list(:values){ playlists } ) @@ -127,7 +127,7 @@ def test_command_list_songs assert_kind_of Array, v, "all hash keys are arrays" assert_equal 5, v.length, "there are 5 values for each hash key" end - assert_equal [2,5,2,2,2], result[:track] + assert_equal [5,6,2,7,2], result[:track] result = @mpd.command_list(:hashes,&twogenres) assert_kind_of Array, result, ":hashes style always returns an array" From 6b25ecbad5ed100171bd343e9f42222819a062ba Mon Sep 17 00:00:00 2001 From: Gavin Kistner Date: Thu, 18 Feb 2016 10:52:07 -0700 Subject: [PATCH 4/9] Command lists are supported; remove docs that say otherwise --- README.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/README.md b/README.md index b6995f1..9b9a235 100644 --- a/README.md +++ b/README.md @@ -247,20 +247,6 @@ Easy as pie. The above will connect to the server like normal, but this time it This section documents the features that are missing in this library at the moment. -### Command lists - -Command lists are not implemented yet. The proposed API would look like: - -```ruby -mpd.command_list do - volume 80 - repeat true - status -end -``` - -What makes me not so eager to implement this is that MPD returns all values one after another. This gets fixed with `command_list_ok_begin`, which returns `list_OK` for every command used, however then we still get more than one response, and I can't think of a reasonable way to retun all of them back to the user. Maybe just ignore the return values? - ### Idle To implement idle, what is needed is a lock that prevents sending commands to the daemon while waiting for the response (except `noidle`). An intermediate solution would be to queue the commands to send them later, when idle has returned the response. From 5d9b5e0ed954b667530481744d42d8305e735e53 Mon Sep 17 00:00:00 2001 From: Gavin Kistner Date: Thu, 18 Feb 2016 11:16:10 -0700 Subject: [PATCH 5/9] Remove unnecessary/unused test code --- test/record_sample_responses.rb | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 test/record_sample_responses.rb diff --git a/test/record_sample_responses.rb b/test/record_sample_responses.rb deleted file mode 100644 index 74799af..0000000 --- a/test/record_sample_responses.rb +++ /dev/null @@ -1,13 +0,0 @@ -require '../lib/ruby-mpd' -require './socket_spoof' - -class RecordingMPD < MPD - def socket - @recording_socket ||= SocketSpoof::Recorder.new(super) - end -end - -m = RecordingMPD.new.tap(&:connect) -begin - m.command_list{ s.each{ |f| addid(f) } } -end \ No newline at end of file From 4d50771471daedb9ce28ea4d00650e1b8c41e2b2 Mon Sep 17 00:00:00 2001 From: Gavin Kistner Date: Thu, 18 Feb 2016 11:17:48 -0700 Subject: [PATCH 6/9] Use spaces for indentation instead of tabs --- test/socket_spoof.rb | 276 +++++++++++++++++++++---------------------- 1 file changed, 138 insertions(+), 138 deletions(-) diff --git a/test/socket_spoof.rb b/test/socket_spoof.rb index d8a9e01..a53d31a 100644 --- a/test/socket_spoof.rb +++ b/test/socket_spoof.rb @@ -7,146 +7,146 @@ # and uses those to decide the (pre-recorded) responses to # yield for subsequent calls to +gets+. module SocketSpoof - # The line in each recording file separating commands and responses - SPLITTER = "--putsabove--getsbelow--\n" + # The line in each recording file separating commands and responses + SPLITTER = "--putsabove--getsbelow--\n" - # Socket wrapper that generates 'recording' files consumed - # by SocketSpoof::Player. - # - # To use, replace your own socket with a call to: - # - # @socket = SocketSpoof::Recorder.new( real_socket ) - # - # This will (by default) create a directory named "socket_recordings" - # and create files within there for each sequence of +puts+ followed - # by one or more gets. - class Recorder - # @param socket [Socket] The real socket to use for communication. - # @param directory [String] The directory to store recording files in. - def initialize(socket,directory:"socket_recordings") - @socket = socket - @commands = [] - FileUtils.mkdir_p( @directory=directory ) - end - def puts(*a) - @socket.puts(*a).tap{ @commands.concat(a.empty? ? [nil] : a) } - end - def gets - @socket.gets.tap do |response| - unless @file && @commands.empty? - @file = File.join( @directory, Digest::SHA256.hexdigest(@commands.inspect) ) - File.open(@file,'w'){ |f| f.puts(@commands); f<] messages previously sent through +puts+ - def last_messages - @current - end - def puts(*a) - @commands.concat(a.empty? ? [nil] : a) - @response_line = -1 - nil # match the return value of IO#puts, just in case - end - def gets - rescan if @auto_update - @current,@commands=@commands,[] unless @commands.empty? - if @responses[@current] - @responses[@current][@response_line+=1] - else - raise "#{self.class} has no recording for #{@current}" - end - end - def method_missing(*a) - raise "#{self.class} has no support for #{a.shift}(#{a.map(&:inspect).join(', ')})" - end + # Find out what messages were last sent to the socket. + # + # Returns an array of strings sent to +puts+ since the + # last time +gets+ was called on the socket. + # @return [Array] messages previously sent through +puts+ + def last_messages + @current + end + def puts(*a) + @commands.concat(a.empty? ? [nil] : a) + @response_line = -1 + nil # match the return value of IO#puts, just in case + end + def gets + rescan if @auto_update + @current,@commands=@commands,[] unless @commands.empty? + if @responses[@current] + @responses[@current][@response_line+=1] + else + raise "#{self.class} has no recording for #{@current}" + end + end + def method_missing(*a) + raise "#{self.class} has no support for #{a.shift}(#{a.map(&:inspect).join(', ')})" + end - private - def rescan - @responses = {} - Dir[File.join(@directory,'*')].each do |file| - commands,responses = File.open(file,'r:utf-8',&:read).split(SPLITTER,2) - if responses - @responses[commands.split("\n")] = responses.lines.to_a - else - warn "#{self.class} ignoring #{file} because it does not appear to have #{SPLITTER.inspect}." - end - end - end - end + private + def rescan + @responses = {} + Dir[File.join(@directory,'*')].each do |file| + commands,responses = File.open(file,'r:utf-8',&:read).split(SPLITTER,2) + if responses + @responses[commands.split("\n")] = responses.lines.to_a + else + warn "#{self.class} ignoring #{file} because it does not appear to have #{SPLITTER.inspect}." + end + end + end + end end \ No newline at end of file From 8fb8177a1f5a2267109ec1c8672e45d169607b41 Mon Sep 17 00:00:00 2001 From: Gavin Kistner Date: Thu, 18 Feb 2016 11:59:30 -0700 Subject: [PATCH 7/9] Attempt to fix continuous integration running test --- test/test_command_lists.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_command_lists.rb b/test/test_command_lists.rb index 060680c..14af75a 100644 --- a/test/test_command_lists.rb +++ b/test/test_command_lists.rb @@ -1,5 +1,5 @@ -require '../lib/ruby-mpd' -require './socket_spoof' +require_relative '../lib/ruby-mpd' +require_relative './socket_spoof' require 'minitest/autorun' class PlaybackMPD < MPD From 364e609e8c7e07a1af52150382d0315f4e2ad208 Mon Sep 17 00:00:00 2001 From: Gavin Kistner Date: Thu, 18 Feb 2016 15:02:26 -0700 Subject: [PATCH 8/9] Create absolute path to recording directory to allow tests to be run from anywhere --- test/test_command_lists.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_command_lists.rb b/test/test_command_lists.rb index 14af75a..a354ed3 100644 --- a/test/test_command_lists.rb +++ b/test/test_command_lists.rb @@ -14,7 +14,8 @@ def last_messages class TestQueue < MiniTest::Unit::TestCase def setup - @mpd = PlaybackMPD.new 'socket_recordings' + spoof_dir = File.expand_path('../socket_recordings',__FILE__) + @mpd = PlaybackMPD.new spoof_dir end def test_songs From b01adf49a40e5593a3dabc7308d7eee9c939e8c2 Mon Sep 17 00:00:00 2001 From: Gavin Kistner Date: Sun, 21 Feb 2016 15:12:37 -0700 Subject: [PATCH 9/9] Slightly improve performance when hundreds of lines are received --- lib/ruby-mpd.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/ruby-mpd.rb b/lib/ruby-mpd.rb index 0c6b546..5ae8908 100644 --- a/lib/ruby-mpd.rb +++ b/lib/ruby-mpd.rb @@ -253,9 +253,10 @@ def callback_thread # @return [true] If "OK" is returned. # @raise [MPDError] If an "ACK" is returned. def handle_server_response + sock = socket # Cache to prevent an extra method call for every response line msg = '' while true - case line = socket.gets + case line = sock.gets when "OK\n", nil break when /^ACK/