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. diff --git a/lib/ruby-mpd.rb b/lib/ruby-mpd.rb index 446ccfb..5ae8908 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 @@ -251,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/ diff --git a/lib/ruby-mpd/parser.rb b/lib/ruby-mpd/parser.rb index eea4b36..190bef2 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) @@ -105,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 @@ -120,8 +129,16 @@ 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| 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. @@ -154,10 +171,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..bd90c47 --- /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 build_playlists parse_response(:listplaylists,response) + 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/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/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..d9a7693 --- /dev/null +++ b/test/socket_recordings/commandlist-playlists @@ -0,0 +1,21 @@ +command_list_begin +listplaylists +command_list_end +--putsabove--getsbelow-- +playlist: Mix Rock Alt 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: 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/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..3cce17f --- /dev/null +++ b/test/socket_recordings/listplaylist @@ -0,0 +1,21 @@ +listplaylistinfo user-gkistner +--putsabove--getsbelow-- +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 +Album: The Ghosts that Haunt Me +Track: 6/10 +Date: 1991 +Genre: Rock +Time: 226 +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 +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..308dd3a --- /dev/null +++ b/test/socket_recordings/listplaylists @@ -0,0 +1,19 @@ +listplaylists +--putsabove--getsbelow-- +playlist: Mix Rock Alt 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: Smooth Town +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..7199188 --- /dev/null +++ b/test/socket_recordings/queue @@ -0,0 +1,65 @@ +playlistinfo +--putsabove--getsbelow-- +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 +Album: God Shuffled His Feet +Track: 1/12 +Date: 1993 +Genre: Alternative & Punk +Time: 310 +Pos: 0 +Id: 1295 +Prio: 2 +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 +Album: The Ghosts that Haunt Me +Track: 6/10 +Date: 1991 +Genre: Rock +Time: 226 +Pos: 1 +Id: 1298 +Prio: 2 +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 +Album: The Ghosts that Haunt Me +Track: 4/10 +Date: 1991 +Genre: Rock +Time: 243 +Pos: 2 +Id: 1299 +Prio: 2 +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 +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: 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..a53d31a --- /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..a354ed3 --- /dev/null +++ b/test/test_command_lists.rb @@ -0,0 +1,185 @@ +require_relative '../lib/ruby-mpd' +require_relative './socket_spoof' +require 'minitest/autorun' + +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 + +class TestQueue < MiniTest::Unit::TestCase + def setup + spoof_dir = File.expand_path('../socket_recordings',__FILE__) + @mpd = PlaybackMPD.new spoof_dir + 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_list_ids + clear_and_add = proc{ + clear + %w[test1.mp3 test\ 2.mp3 test3.mp3].each{ |f| addid(f) } + } + + 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 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'), + Time.iso8601('2015-11-20T15:32:30Z'), Time.iso8601('2015-11-20T15:54:49Z'), + ] + }, + @mpd.command_list(:hash){ playlists } + ) + + assert_equal( + [ + { 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: "Smooth Town", :"last-modified" => Time.iso8601('2015-11-20T15:54:49Z') }, + ], + @mpd.command_list(:hashes){ playlists } + ) + + assert_equal( + [ + "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'), + "Smooth Town", Time.iso8601('2015-11-20T15:54:49Z'), + ], + @mpd.command_list(:values){ playlists } + ) + + 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 [5,6,2,7,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