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 8e35401..8ce464e 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 @@ -187,15 +189,19 @@ def ping # @raise [MPDError] if the command failed. def send_command(command, *args) raise ConnectionError, "Not connected to the server!" unless socket - - @mutex.synchronize do - begin - socket.puts convert_command(command, *args) - response = handle_server_response - return parse_response(command, response) - rescue Errno::EPIPE, ConnectionError - reconnect - retry + if @command_list_commands + @command_list_commands << command + socket.puts convert_command(command, *args) + else + @mutex.synchronize do + begin + socket.puts convert_command(command, *args) + response = handle_server_response + parse_response(command, response) + rescue Errno::EPIPE, ConnectionError + reconnect + retry + end end end end @@ -251,9 +257,11 @@ def callback_thread # @return [true] If "OK" is returned. # @raise [MPDError] If an "ACK" is returned. def handle_server_response + raise "Cannot read from the server during a command list" if @command_list_active + 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" break when /^ACK/ diff --git a/lib/ruby-mpd/parser.rb b/lib/ruby-mpd/parser.rb index 8992970..e3bc061 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} " @@ -55,12 +57,19 @@ def quotable_param(value) FLOAT_KEYS = Set[:mixrampdb, :elapsed] BOOL_KEYS = Set[:repeat, :random, :single, :consume, :outputenabled] - # Commands, where it makes sense to always explicitly return an array. + # Commands where it makes sense to always explicitly return an array. RETURN_ARRAY = Set[:channels, :outputs, :readmessages, :list, :listallinfo, :find, :search, :listplaylists, :listplaylist, :playlistfind, :playlistsearch, :plchanges, :tagtypes, :commands, :notcommands, :urlhandlers, :decoders, :listplaylistinfo, :playlistinfo] + # Commands that should always return MPD::Song instances + SONG_COMMANDS = Set[:listallinfo,:playlistinfo,:find,:findadd,:search, + :searchadd,:playlistfind,:playlistsearch,:plchanges,:listplaylistinfo] + + # Commands that should always return MPD::Playlist instances + PLAYLIST_COMMANDS = Set[:listplaylists] + # Parses key-value pairs into correct class. def parse_key(key, value) if INT_KEYS.include? key @@ -105,25 +114,26 @@ 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 end end - # 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) } - end - # Make chunks from string. # @return [Array] def make_chunks(string) @@ -150,17 +160,39 @@ def parse_response(command, string) build_response(command, string) end + def parse_command_list(commands, string) + [].tap do |results| + string.split("list_OK\n").each do |str| + command = commands.shift + results << parse_response(command, str) unless str.empty? + end + end + end + # Parses the response into appropriate objects (either a single object, # 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" } + + make_song = SONG_COMMANDS.include?(command) + make_plist = PLAYLIST_COMMANDS.include?(command) + make_hash = force_hash || make_song || make_plist || 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 + result << (make_hash ? build_hash(chunk) : parse_line(chunk)[1]) # parse_line(chunk)[1] is object + end + + if make_song + list.map! do |opts| + if opts[:file] && opts[:file] =~ %r{^https?://}i + opts = { file:opts[:file], time:[0] } + end + Song.new(@mpd, opts) + end + elsif make_plist + list.map!{ |opts| Playlist.new(self,opts) } end # if list has only one element and not set to explicit array, return it, else return array diff --git a/lib/ruby-mpd/playlist.rb b/lib/ruby-mpd/playlist.rb index 4382bcd..9d09ff6 100644 --- a/lib/ruby-mpd/playlist.rb +++ b/lib/ruby-mpd/playlist.rb @@ -24,14 +24,7 @@ def initialize(mpd, options) # Lists the songs in the playlist. Playlist plugins are supported. # @return [Array] songs in the playlist. def songs - result = @mpd.send_command(:listplaylistinfo, @name) - result.map do |hash| - if hash[:file] && !hash[:file].match(/^(https?:\/\/)?/)[0].empty? - Song.new(@mpd, {:file => hash[:file], :time => [0]}) - else - Song.new(@mpd, hash) - end - end + @mpd.send_command(:listplaylistinfo, @name) rescue TypeError puts "Files inside Playlist '#{@name}' do not exist!" return [] @@ -77,10 +70,10 @@ def delete(pos) @mpd.send_command :playlistdelete, @name, pos end - # Moves song with SONGID in the playlist to the position SONGPOS. + # Move a song in the playlist to a new 0-based index. # @macro returnraise - def move(songid, songpos) - @mpd.send_command :playlistmove, @name, songid, songpos + def move(from_index, to_index) + @mpd.send_command :playlistmove, @name, from_index, to_index end # Renames the playlist to +new_name+. diff --git a/lib/ruby-mpd/plugins/command_list.rb b/lib/ruby-mpd/plugins/command_list.rb new file mode 100644 index 0000000..ea4bfc5 --- /dev/null +++ b/lib/ruby-mpd/plugins/command_list.rb @@ -0,0 +1,99 @@ +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 +{results:true}+ to the method. + # + # 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 +results+ is +true+. + # + # @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(results:true){ my_songs.each{ |song| addid(song) } } + # #=> [799,800,801,802,803] + # + # @example Finding songs matching various genres + # results = @mpd.command_list(results:true) do + # where genre:'Alternative Rock' + # where genre:'Alt. Rock' + # where genre:'alt-rock' + # end + # p results.class #=> Array (One result for each command that gives results) + # p results.length #=> 3 (One for each command that returns results) + # p results.first.class #=> Array (Each `where` command returns its own results) + # + # + # @example Using playlists inside a command list + # def shuffle_playlist( playlist ) + # song_count = @mpd.send_command(:listplaylist, playlist.name).length + # @mpd.command_list do + # (song_count-1).downto(1){ |i| playlist.move i, rand(i+1) } + # end + # end + # + # + # @see CommandList::Commands CommandList::Commands for a list of supported commands. + def command_list(opts={},&commands) + @mutex.synchronize do + begin + @command_list_commands = [] + socket.puts( opts[:results] ? "command_list_ok_begin" : "command_list_begin" ) + @command_list_active = true + CommandList::Commands.new(self).instance_eval(&commands) + @command_list_active = false + socket.puts "command_list_end" + + # clear the response from the socket, even if we will not parse it + response = handle_server_response || "" + + parse_command_list( @command_list_commands, response ) if opts[:results] + rescue Errno::EPIPE + reconnect + retry + ensure + @command_list_commands = nil + @command_list_active = false + 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 + include MPD::Plugins::Playlists + + private + def send_command(command,*args) + @mpd.send_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..c46d943 100644 --- a/lib/ruby-mpd/plugins/database.rb +++ b/lib/ruby-mpd/plugins/database.rb @@ -35,7 +35,7 @@ def files(path = nil) # # @return [Array] def songs(path = nil) - build_songs_list send_command(:listallinfo, path) + send_command(:listallinfo, path) end # lsinfo - Clients that are connected via UNIX domain socket may use this @@ -92,12 +92,7 @@ def where(params, options = {}) command = options[:strict] ? :find : :search end - response = send_command(command, params) - if response == true - return true - else - build_songs_list response - end + send_command(command, params) end # Tell the server to update the database. Optionally, diff --git a/lib/ruby-mpd/plugins/playlists.rb b/lib/ruby-mpd/plugins/playlists.rb index 8f69c48..95218b0 100644 --- a/lib/ruby-mpd/plugins/playlists.rb +++ b/lib/ruby-mpd/plugins/playlists.rb @@ -7,7 +7,7 @@ module Playlists # # @return [Array] Array of playlists def playlists - send_command(:listplaylists).map { |opt| MPD::Playlist.new(self, opt) } + send_command(:listplaylists) end end diff --git a/lib/ruby-mpd/plugins/queue.rb b/lib/ruby-mpd/plugins/queue.rb index 2c34ab4..d526bbc 100644 --- a/lib/ruby-mpd/plugins/queue.rb +++ b/lib/ruby-mpd/plugins/queue.rb @@ -12,7 +12,7 @@ module Queue # @return [Array] Array of songs in the queue # or a single song. def queue(limit=nil) - build_songs_list send_command(:playlistinfo, limit) + send_command(:playlistinfo, limit) end # Add the file _path_ to the queue. If path is a directory, @@ -120,13 +120,13 @@ def queue_search(type, what, options = {}) # @return [Array] Songs that matched. def queue_where(params, options = {}) command = options[:strict] ? :playlistfind : :playlistsearch - build_songs_list send_command(command, params) + send_command(command, params) end # List the changes since the specified version in the queue. # @return [Array] def queue_changes(version) - build_songs_list send_command(:plchanges, version) + send_command(:plchanges, version) end # plchangesposid 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/spec/ruby-mpd/parser_spec.rb b/spec/ruby-mpd/parser_spec.rb index 095df17..657034c 100644 --- a/spec/ruby-mpd/parser_spec.rb +++ b/spec/ruby-mpd/parser_spec.rb @@ -124,12 +124,6 @@ end end - describe "#build_songs_list" do - context "when passed an empty array" do - it { expect(subject.send(:build_songs_list, [])).to eql([]) } - end - end - describe "#parse_response" do context "when passed listall command" do let(:command) { :listall } @@ -141,8 +135,12 @@ context "when passed listallinfo command" do let(:command) { :listallinfo } let(:str) { "Directory: xxxx\nFile: file1\nFile: file2\nFile: file3\nFile: file4\n" } + let(:song1){ MPD::Song.new(subject,{file:'file1'}) } + let(:song2){ MPD::Song.new(subject,{file:'file2'}) } + let(:song3){ MPD::Song.new(subject,{file:'file3'}) } + let(:song4){ MPD::Song.new(subject,{file:'file4'}) } it { expect(subject.send(:parse_response, command, str)) - .to eql(["file1", "file2", "file3", "file4"]) } + .to eql([song1,song2,song3,song4]) } end context "when passed unknown command with empty string" do @@ -160,14 +158,9 @@ context "when passed valid command and a string of elements" do let(:command) { :find } let(:str) { "title: Shelter\nTrack: 7\nxfade: 0\nstate: play\n" } + let(:song){ [MPD::Song.new( nil, title:'Shelter', track:7, xfade:0, state: :play )] } it { expect(subject.send(:parse_response, command, str)) - .to eql([{:title=>"Shelter", :track=>7, :xfade=>0, :state=>:play}]) } - end - - context "when passed valid command and a single element" do - let(:command) { :find } - let(:str) { "title: Shelter\n" } - it { expect(subject.send(:parse_response, command, str)).to eql(["Shelter"]) } + .to eql(song) } end context "when passed invalid command and a single element" do diff --git a/spec/ruby-mpd/plugins/database_spec.rb b/spec/ruby-mpd/plugins/database_spec.rb index a4158af..00e4dbe 100644 --- a/spec/ruby-mpd/plugins/database_spec.rb +++ b/spec/ruby-mpd/plugins/database_spec.rb @@ -47,7 +47,6 @@ context "when given all args" do it "should send correct params" do expect(subject).to receive(:send_command).with(:listallinfo, 'path').and_return('result') - expect(subject).to receive(:build_songs_list).with('result') subject.songs('path') end end @@ -55,7 +54,6 @@ context "when given one arg" do it "should send correct params" do expect(subject).to receive(:send_command).with(:listallinfo, nil).and_return('result') - expect(subject).to receive(:build_songs_list).with('result') subject.songs end end @@ -90,8 +88,7 @@ context "when given no options and response is not true" do it "should send correct params" do expect(subject).to receive(:send_command).with(:search, 'params').and_return('xxxxx') - expect(subject).to receive(:build_songs_list).with('xxxxx').and_return('yyyyy') - expect(subject.where('params', {})).to eql('yyyyy') + expect(subject.where('params', {})).to eql('xxxxx') end end diff --git a/spec/ruby-mpd/plugins/playlists_spec.rb b/spec/ruby-mpd/plugins/playlists_spec.rb deleted file mode 100644 index ea5c3fe..0000000 --- a/spec/ruby-mpd/plugins/playlists_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'spec_helper' -require_relative '../../../lib/ruby-mpd' - -RSpec.describe MPD::Plugins::Playlists do - subject { MPD.new } - - describe "#playlists" do - let(:pl1) { 'playlist1' } - let(:pl2) { 'playlist2' } - let(:pls) { ['opts1', 'opts2'] } - - it "should send correct params" do - expect(MPD::Playlist).to receive(:new).with(subject, pls.first).and_return(pl1) - expect(MPD::Playlist).to receive(:new).with(subject, pls.last).and_return(pl2) - expect(subject).to receive(:send_command).with(:listplaylists).and_return(pls) - expect(subject.playlists).to eql([pl1, pl2]) - end - end -end diff --git a/spec/ruby-mpd/plugins/queue_spec.rb b/spec/ruby-mpd/plugins/queue_spec.rb index 16fb81f..dd73956 100644 --- a/spec/ruby-mpd/plugins/queue_spec.rb +++ b/spec/ruby-mpd/plugins/queue_spec.rb @@ -8,7 +8,6 @@ context "when pass a limit" do it "should send correct params" do expect(subject).to receive(:send_command).with(:playlistinfo, 'limit').and_return('result') - expect(subject).to receive(:build_songs_list).with('result') subject.queue('limit') end end @@ -16,7 +15,6 @@ context "when not passed a limit" do it "should send correct params" do expect(subject).to receive(:send_command).with(:playlistinfo, nil).and_return('result') - expect(subject).to receive(:build_songs_list).with('result') subject.queue end end @@ -142,7 +140,6 @@ expect(subject).to receive(:send_command) .with(:playlistfind, 'params') .and_return('result') - expect(subject).to receive(:build_songs_list).with('result') subject.queue_where('params', {strict: true}) end end @@ -153,7 +150,6 @@ expect(subject).to receive(:send_command) .with(:plchanges, 'version') .and_return('result') - expect(subject).to receive(:build_songs_list).with('result') subject.queue_changes('version') end end diff --git a/test/_helper.rb b/test/_helper.rb new file mode 100644 index 0000000..66a2014 --- /dev/null +++ b/test/_helper.rb @@ -0,0 +1,13 @@ +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 diff --git a/test/socket_recordings/commandlist-addid-ok b/test/socket_recordings/commandlist-addid-ok new file mode 100644 index 0000000..75fe1f9 --- /dev/null +++ b/test/socket_recordings/commandlist-addid-ok @@ -0,0 +1,7 @@ +command_list_ok_begin +addid test1.mp3 +command_list_end +--putsabove--getsbelow-- +Id: 107 +list_OK +OK 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-clear_addid-ok b/test/socket_recordings/commandlist-clear_addid-ok new file mode 100644 index 0000000..6d3c2aa --- /dev/null +++ b/test/socket_recordings/commandlist-clear_addid-ok @@ -0,0 +1,15 @@ +command_list_ok_begin +clear +addid test1.mp3 +addid "test 2.mp3" +addid test3.mp3 +command_list_end +--putsabove--getsbelow-- +list_OK +Id: 107 +list_OK +Id: 108 +list_OK +Id: 109 +list_OK +OK diff --git a/test/socket_recordings/commandlist-empty b/test/socket_recordings/commandlist-empty new file mode 100644 index 0000000..ecd03d7 --- /dev/null +++ b/test/socket_recordings/commandlist-empty @@ -0,0 +1,4 @@ +command_list_begin +command_list_end +--putsabove--getsbelow-- +OK diff --git a/test/socket_recordings/commandlist-empty-ok b/test/socket_recordings/commandlist-empty-ok new file mode 100644 index 0000000..a897153 --- /dev/null +++ b/test/socket_recordings/commandlist-empty-ok @@ -0,0 +1,4 @@ +command_list_ok_begin +command_list_end +--putsabove--getsbelow-- +OK diff --git a/test/socket_recordings/commandlist-mutateplaylist-ok b/test/socket_recordings/commandlist-mutateplaylist-ok new file mode 100644 index 0000000..64e7340 --- /dev/null +++ b/test/socket_recordings/commandlist-mutateplaylist-ok @@ -0,0 +1,28 @@ +command_list_ok_begin +playlistclear temp +playlistadd temp song2.mp3 +playlistadd temp dummy.mp3 +playlistadd temp song1.mp3 +playlistadd temp song3.mp3 +playlistdelete temp 1 +playlistmove temp 1 0 +listplaylistinfo temp +rm temp +command_list_end +--putsabove--getsbelow-- +list_OK +list_OK +list_OK +list_OK +list_OK +list_OK +list_OK +file: song1.mp3 +Time: 296 +file: song2.mp3 +Time: 308 +file: song3.mp3 +Time: 152 +list_OK +list_OK +OK diff --git a/test/socket_recordings/commandlist-playlist-ok b/test/socket_recordings/commandlist-playlist-ok new file mode 100644 index 0000000..a2cc590 --- /dev/null +++ b/test/socket_recordings/commandlist-playlist-ok @@ -0,0 +1,58 @@ +command_list_ok_begin +listplaylistinfo Thump +command_list_end +--putsabove--getsbelow-- +file: gavin/Muse/The 2nd Law (Deluxe Version)/02 Madness.m4a +Last-Modified: 2013-11-17T22:30:54Z +Artist: Muse +Album: The 2nd Law (Deluxe Version) +Title: Madness +Track: 2/13 +Genre: Alternative +Date: 2012-10-01T07:00:00Z +Disc: 1/1 +AlbumArtist: Muse +Time: 280 +file: gavin/U2/The Best Of 1990-2000/15 Numb (New Mix).mp3 +Last-Modified: 2003-09-24T14:31:05Z +Artist: U2 +Title: Numb (New Mix) +Album: The Best Of 1990-2000 +Track: 15/16 +Date: 2002 +Genre: Rock +Composer: U2 +Time: 264 +file: gavin/Massive Attack/Mezzanine/03 Teardrop.m4a +Last-Modified: 2012-09-05T19:38:14Z +Artist: Massive Attack +Album: Mezzanine +Title: Teardrop +Track: 3/11 +Genre: Electronic +Date: 1998-04-20T07:00:00Z +Disc: 1/1 +AlbumArtist: Massive Attack +Time: 331 +file: gavin/Massive Attack/Mezzanine/01 Angel.m4a +Last-Modified: 2012-09-05T19:38:17Z +Artist: Massive Attack +Album: Mezzanine +Title: Angel +Track: 1/11 +Genre: Electronic +Date: 1998-04-20T07:00:00Z +Disc: 1/1 +AlbumArtist: Massive Attack +Time: 380 +file: gavin/Eels/Shrek 2/07 I Need Some Sleep.mp3 +Last-Modified: 2005-01-14T21:24:25Z +Artist: Eels +Title: I Need Some Sleep +Album: Shrek 2 +Track: 7/14 +Date: 2004 +Genre: Soundtrack +Time: 147 +list_OK +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-playlists-ok b/test/socket_recordings/commandlist-playlists-ok new file mode 100644 index 0000000..2f16c2e --- /dev/null +++ b/test/socket_recordings/commandlist-playlists-ok @@ -0,0 +1,22 @@ +command_list_ok_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 +list_OK +OK diff --git a/test/socket_recordings/commandlist-where-multi b/test/socket_recordings/commandlist-where-multi new file mode 100644 index 0000000..47342fd --- /dev/null +++ b/test/socket_recordings/commandlist-where-multi @@ -0,0 +1,66 @@ +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 +OK diff --git a/test/socket_recordings/commandlist-where-multi-ok b/test/socket_recordings/commandlist-where-multi-ok new file mode 100644 index 0000000..3a694ed --- /dev/null +++ b/test/socket_recordings/commandlist-where-multi-ok @@ -0,0 +1,68 @@ +command_list_ok_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 +list_OK +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 +list_OK +OK diff --git a/test/socket_recordings/commandlist-where-single b/test/socket_recordings/commandlist-where-single new file mode 100644 index 0000000..09214d5 --- /dev/null +++ b/test/socket_recordings/commandlist-where-single @@ -0,0 +1,17 @@ +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 +OK diff --git a/test/socket_recordings/commandlist-where-single-ok b/test/socket_recordings/commandlist-where-single-ok new file mode 100644 index 0000000..9cc3ebd --- /dev/null +++ b/test/socket_recordings/commandlist-where-single-ok @@ -0,0 +1,18 @@ +command_list_ok_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 +list_OK +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-filesonly b/test/socket_recordings/listplaylist-filesonly new file mode 100644 index 0000000..add0696 --- /dev/null +++ b/test/socket_recordings/listplaylist-filesonly @@ -0,0 +1,5 @@ +listplaylistinfo filesonly +--putsabove--getsbelow-- +file: Crash Test Dummies/The Ghosts that Haunt Me/06 The Ghosts That Haunt Me.mp3 +file: Crash Test Dummies/The Ghosts that Haunt Me/04 The Country Life.mp3 +OK diff --git a/test/socket_recordings/listplaylist-onesong b/test/socket_recordings/listplaylist-onesong new file mode 100644 index 0000000..182012e --- /dev/null +++ b/test/socket_recordings/listplaylist-onesong @@ -0,0 +1,12 @@ +listplaylistinfo onesong +--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 +OK diff --git a/test/socket_recordings/listplaylist-twosongs b/test/socket_recordings/listplaylist-twosongs new file mode 100644 index 0000000..7abeb76 --- /dev/null +++ b/test/socket_recordings/listplaylist-twosongs @@ -0,0 +1,21 @@ +listplaylistinfo twosongs +--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..d1e7281 --- /dev/null +++ b/test/test_command_lists.rb @@ -0,0 +1,105 @@ +require_relative './_helper' + +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(results:true,&clear_and_add) + assert_equal([107,108,109], ids) + + id = @mpd.command_list(results:true){ addid 'test1.mp3' } + assert_equal([107], id, "single command produces single-valued array") + 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" + ) + + lists = @mpd.command_list(results:true){ playlists }.first + 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'" + + songs = @mpd.command_list(results:true) do + temp = MPD::Playlist.new(@mpd,'temp') + temp.clear + temp.add 'song2.mp3' + temp.add 'dummy.mp3' + temp.add 'song1.mp3' + temp.add 'song3.mp3' + temp.delete 1 + temp.move 1, 0 + temp.songs + temp.destroy + end + assert_kind_of Array, songs, "The command list returns an array" + assert_equal 1, songs.length, "Only the `songs` command should produce results" + songs = songs.first + assert_equal 3, songs.length, "There are three songs in the result" + assert songs.all?{ |value| value.is_a? MPD::Song }, "Every return value is a song" + 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(results:true,&twogenres) + assert_kind_of Array, result, "command_list with results always returns an array" + assert_equal 2, result.size, "two commands yield two results" + assert_kind_of Array, result[0], "where always returns an array" + assert_kind_of Array, result[1], "where always returns an array" + assert_equal 5, result.flatten.size, "there are 5 songs total in the result set" + result.flatten.each{ |v| assert_kind_of MPD::Song, v, "all results are songs" } + 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(results:true,&onegenre) + assert_kind_of Array, result, "command_list with results always returns an array" + assert_equal 1, result.size, "one command yields one result" + assert_kind_of Array, result.first, "`where` returns an array, even if only one value" + assert_equal 1, result.first.size, "there is 1 song in the result set" + result.first.each{ |v| assert_kind_of MPD::Song, v, "all results are songs" } + end + +end \ No newline at end of file diff --git a/test/test_parser.rb b/test/test_parser.rb index 5152329..3cd4203 100644 --- a/test/test_parser.rb +++ b/test/test_parser.rb @@ -1,5 +1,4 @@ -require_relative '../lib/ruby-mpd' -require 'minitest/autorun' +require_relative './_helper' Parser = Class.new do include MPD::Parser @@ -7,15 +6,10 @@ Parser.send(:public, *MPD::Parser.private_instance_methods) class TestParser < MiniTest::Test - def setup @parser = Parser.new end - def teardown - - end - # Conversions for commands to the server def test_convert_bool assert_equal @parser.convert_command(:pause, true), 'pause 1' @@ -49,4 +43,31 @@ def test_parse_playlist_name assert_equal @parser.parse_key(:playlist, 'leftover/classics.m3u'), 'leftover/classics.m3u' end -end \ No newline at end of file +end + +class TestParsingSongs < MiniTest::Test + def setup + spoof_dir = File.expand_path('../socket_recordings',__FILE__) + @mpd = PlaybackMPD.new spoof_dir + end + def test_playlist_songs + songs = MPD::Playlist.new( @mpd, 'onesong' ).songs + assert_kind_of Array, songs + assert_equal 1, songs.length + assert songs.all?{ |value| value.is_a? MPD::Song }, "Every return value is a Song" + assert_equal songs.first.track_length, 226 + + songs = MPD::Playlist.new( @mpd, 'twosongs' ).songs + assert_kind_of Array, songs + assert_equal 2, songs.length + assert songs.all?{ |value| value.is_a? MPD::Song }, "Every return value is a Song" + assert_equal songs.first.track_length, 226 + + songs = MPD::Playlist.new( @mpd, 'filesonly' ).songs + assert_kind_of Array, songs + assert_equal 2, songs.length + assert songs.all?{ |value| value.is_a? MPD::Song }, "Every return value is a Song" + assert_equal songs.first.file, "Crash Test Dummies/The Ghosts that Haunt Me/06 The Ghosts That Haunt Me.mp3" + assert_nil songs.first.track_length + end +end