ruby-mpd 0.1.7 → 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -3,7 +3,7 @@
3
3
  ruby-mpd is a powerful object-oriented Music Player Daemon library, forked from librmpd.
4
4
  librmpd is as of writing outdated by 6 years! This library tries to act as a successor,
5
5
  originally using librmpd as a base, however almost all of the codebase was rewritten.
6
- ruby-mpd supports all "modern" MPD features.
6
+ ruby-mpd supports all "modern" MPD features as well as callbacks.
7
7
 
8
8
  == MPD Protocol
9
9
 
@@ -41,7 +41,7 @@ When you are done, disconnect by calling disconnect.
41
41
 
42
42
  *Note*: The server may disconnect you at any time due to inactivity. This can
43
43
  be fixed by enabling callbacks (see the Callbacks section) or by issuing a
44
- `ping` command at certain intervals
44
+ `ping` command at certain intervals.
45
45
 
46
46
  Once connected, you can issue commands to talk to the server.
47
47
 
data/lib/ruby-mpd.rb CHANGED
@@ -46,16 +46,20 @@ class MPD
46
46
  def initialize(hostname = 'localhost', port = 6600)
47
47
  @hostname = hostname
48
48
  @port = port
49
- @socket = nil
50
- @version = nil
51
- @tags = nil
49
+ reset_vars
52
50
 
53
- @stop_cb_thread = false
54
51
  @mutex = Mutex.new
55
- @cb_thread = nil
56
52
  @callbacks = {}
57
53
  end
58
54
 
55
+ # Initialize instance variables on new object, or on disconnect.
56
+ def reset_vars
57
+ @socket = nil
58
+ @version = nil
59
+ @tags = nil
60
+ end
61
+ private :reset_vars
62
+
59
63
  # This will register a block callback that will trigger whenever
60
64
  # that specific event happens.
61
65
  #
@@ -81,12 +85,51 @@ class MPD
81
85
  # @param [Symbol] event The event that happened.
82
86
  # @return [void]
83
87
  def emit(event, *args)
84
- @callbacks[event] ||= []
88
+ return unless @callbacks[event]
85
89
  @callbacks[event].each do |handle|
86
90
  handle.call *args
87
91
  end
88
92
  end
89
93
 
94
+ # Constructs a callback loop thread and/or resumes it.
95
+ # @return [Thread]
96
+ def callback_thread
97
+ @cb_thread ||= Thread.new(self) do |mpd|
98
+ old_status = {}
99
+ while true
100
+ status = mpd.status rescue {}
101
+
102
+ status[:connection] = mpd.connected?
103
+
104
+ status[:time] = [nil, nil] if !status[:time] # elapsed, total
105
+ status[:audio] = [nil, nil, nil] if !status[:audio] # samp, bits, chans
106
+
107
+ status.each do |key, val|
108
+ next if val == old_status[key] # skip unchanged keys
109
+
110
+ if key == :song
111
+ emit(:song, mpd.current_song)
112
+ else # convert arrays to splat arguments
113
+ val.is_a?(Array) ? emit(key, *val) : emit(key, val)
114
+ end
115
+ end
116
+
117
+ old_status = status
118
+ sleep 0.1
119
+
120
+ if !status[:connection] && !Thread.current[:stop]
121
+ sleep 2
122
+ mpd.connect rescue nil
123
+ end
124
+
125
+ Thread.stop if Thread.current[:stop]
126
+ end
127
+ end
128
+ @cb_thread[:stop] = false
129
+ @cb_thread.run if @cb_thread.stop?
130
+ end
131
+ private :callback_thread
132
+
90
133
  # Connect to the daemon.
91
134
  #
92
135
  # When called without any arguments, this will just connect to the server
@@ -104,47 +147,7 @@ class MPD
104
147
  @socket = File.exists?(@hostname) ? UNIXSocket.new(@hostname) : TCPSocket.new(@hostname, @port)
105
148
  @version = @socket.gets.chomp.gsub('OK MPD ', '') # Read the version
106
149
 
107
- if callbacks and (@cb_thread.nil? or !@cb_thread.alive?)
108
- @stop_cb_thread = false
109
- @cb_thread = Thread.new(self) { |mpd|
110
- old_status = {}
111
- connected = ''
112
- while !@stop_cb_thread
113
- status = mpd.status rescue {}
114
- c = mpd.connected?
115
-
116
- # @todo Move into status[:connection]?
117
- if connected != c
118
- connected = c
119
- emit(:connection, connected)
120
- end
121
-
122
- status[:time] = [nil, nil] if !status[:time] # elapsed, total
123
- status[:audio] = [nil, nil, nil] if !status[:audio] # samp, bits, chans
124
-
125
- status.each do |key, val|
126
- next if val == old_status[key] # skip unchanged keys
127
-
128
- if key == :song
129
- emit(:song, mpd.current_song)
130
- else # convert arrays to splat arguments
131
- val.is_a?(Array) ? emit(key, *val) : emit(key, val)
132
- end
133
- end
134
-
135
- old_status = status
136
- sleep 0.1
137
-
138
- if !connected
139
- sleep 2
140
- unless @stop_cb_thread
141
- mpd.connect rescue nil
142
- end
143
- end
144
- end
145
- }
146
- end
147
-
150
+ callback_thread if callbacks
148
151
  return true
149
152
  end
150
153
 
@@ -163,13 +166,13 @@ class MPD
163
166
  # the callback thread, thus disabling callbacks.
164
167
  # @return [Boolean] True if successfully disconnected, false otherwise.
165
168
  def disconnect
166
- @stop_cb_thread = true
169
+ @cb_thread[:stop] = true if @cb_thread
167
170
 
168
171
  return false if !@socket
169
172
 
170
173
  @socket.puts 'close'
171
174
  @socket.close
172
- @socket = nil
175
+ reset_vars
173
176
  return true
174
177
  end
175
178
 
@@ -202,7 +205,7 @@ class MPD
202
205
  # @return (see #handle_server_response)
203
206
  # @raise [MPDError] if the command failed.
204
207
  def send_command(command, *args)
205
- raise ConnectionError, "Not connected to the server!" if @socket.nil?
208
+ raise ConnectionError, "Not connected to the server!" if !@socket
206
209
 
207
210
  @mutex.synchronize do
208
211
  begin
@@ -210,7 +213,7 @@ class MPD
210
213
  response = handle_server_response
211
214
  return parse_response(command, response)
212
215
  rescue Errno::EPIPE
213
- @socket = nil
216
+ reset_vars # kill the socket and reset
214
217
  raise ConnectionError, 'Broken pipe (got disconnected)'
215
218
  end
216
219
  end
@@ -225,21 +228,16 @@ class MPD
225
228
  # @return [true] If "OK" is returned.
226
229
  # @raise [MPDError] If an "ACK" is returned.
227
230
  def handle_server_response
228
- return if @socket.nil?
229
-
230
231
  msg = ''
231
- reading = true
232
- error = nil
233
- while reading
234
- line = @socket.gets
235
- case line
232
+ while true
233
+ case line = @socket.gets
236
234
  when "OK\n", nil
237
- reading = false
235
+ break
238
236
  when /^ACK/
239
237
  error = line
240
- reading = false
238
+ break
241
239
  else
242
- msg += line
240
+ msg << line
243
241
  end
244
242
  end
245
243
 
@@ -12,7 +12,7 @@ class MPD
12
12
  class ServerArgumentError < ServerError; end
13
13
  # MPD server password incorrect - ACK_ERROR_PASSWORD
14
14
  class IncorrectPassword < ServerError; end
15
- # ACK_ERROR_PERMISSION - not permitted to use the command.
15
+ # ACK_ERROR_PERMISSION - not permitted to use the command.
16
16
  # (Mostly, the solution is to connect via UNIX domain socket)
17
17
  class PermissionError < ServerError; end
18
18
 
@@ -30,4 +30,4 @@ class MPD
30
30
  class NotPlaying < ServerError; end
31
31
  # ACK_ERROR_EXIST - the resource already exists.
32
32
  class AlreadyExists < ServerError; end
33
- end
33
+ end
@@ -40,7 +40,7 @@ class MPD
40
40
 
41
41
 
42
42
  # Commands, where it makes sense to always explicitly return an array.
43
- RETURN_ARRAY = [:channels, :outputs, :readmessages, :list, :listall,
43
+ RETURN_ARRAY = [:channels, :outputs, :readmessages, :list, :listall,
44
44
  :listallinfo, :find, :search, :listplaylists, :listplaylist, :playlistfind,
45
45
  :playlistsearch, :plchanges, :tagtypes, :commands, :notcommands, :urlhandlers,
46
46
  :decoders, :listplaylistinfo]
@@ -57,8 +57,8 @@ class MPD
57
57
  value.to_sym
58
58
  elsif key == :playlist && !value.to_i.zero?
59
59
  # doc states it's an unsigned int, meaning if we get 0,
60
- # then it's a name string. HAXX! what if playlist name is '123'?
61
- # @todo HAXX
60
+ # then it's a name string.
61
+ # @todo HAXX! what if playlist name is '123'?
62
62
  value.to_i
63
63
  elsif key == :db_update
64
64
  Time.at(value.to_i)
@@ -71,13 +71,12 @@ class MPD
71
71
  end
72
72
  end
73
73
 
74
- # Parses a single response line into an object.
75
- def parse_line(string)
76
- return nil if string.nil?
77
- key, value = string.split(': ', 2)
74
+ # Parses a single response line into a key-object (value) pair.
75
+ def parse_line(line)
76
+ return nil if line.nil?
77
+ key, value = line.split(/:\s?/, 2)
78
78
  key = key.downcase.to_sym
79
- value ||= '' # no nil values please ("album: ")
80
- return parse_key(key, value.chomp)
79
+ return key, parse_key(key, value.chomp)
81
80
  end
82
81
 
83
82
  # This builds a hash out of lines returned from the server,
@@ -88,16 +87,14 @@ class MPD
88
87
  return {} if string.nil?
89
88
 
90
89
  string.split("\n").each_with_object({}) do |line, hash|
91
- key, value = line.split(': ', 2)
92
- key = key.downcase.to_sym
93
- value ||= '' # no nil values please ("album: ")
90
+ key, object = parse_line(line)
94
91
 
95
92
  # if val appears more than once, make an array of vals.
96
93
  if hash.include? key
97
- hash[key] = [hash[key]] if !hash[key].is_a?(Array) # if necessary
98
- hash[key] << parse_key(key, value.chomp) # add new val to array
94
+ hash[key] = Array(hash[key]) # convert to array (http://rubyquicktips.com/post/9618833891/convert-object-to-array)
95
+ hash[key] << object # add new obj to array
99
96
  else # val hasn't appeared yet, map it.
100
- hash[key] = parse_key(key, value.chomp) # map val to key
97
+ hash[key] = object # map obj to key
101
98
  end
102
99
  end
103
100
  end
@@ -109,10 +106,15 @@ class MPD
109
106
  return array.map {|hash| Song.new(hash) }
110
107
  end
111
108
 
109
+ # Remove lines which we don't want.
110
+ def filter_lines(string, filter)
111
+ string.split("\n").reject {|line| line =~ /(#{filter.join('|')}):/}.join("\n")
112
+ end
113
+
112
114
  # Make chunks from string.
113
115
  # @return [Array<String>]
114
116
  def make_chunks(string)
115
- first_key = string.match(/\A(.+?): /)[1]
117
+ first_key = string.match(/\A(.+?):\s?/)[1]
116
118
 
117
119
  chunks = string.split(/\n(?=#{first_key})/)
118
120
  chunks.inject([]) do |result, chunk|
@@ -121,28 +123,37 @@ class MPD
121
123
  end
122
124
 
123
125
  # Parses the response, determining per-command on what parsing logic
124
- # to use (build_response vs build_grouped_response).
126
+ # to use (build_response vs build a single grouped hash).
125
127
  #
126
128
  # @return [Array<Hash>, Array<String>, String, Integer] Parsed response.
127
129
  def parse_response(command, string)
128
130
  # return explicit array if needed
129
- return RETURN_ARRAY.include?(command) ? [] : true if string.is_a?(TrueClass)
130
- command == :listall ? build_grouped_response(string) : build_response(command, string)
131
+ return RETURN_ARRAY.include?(command) ? [] : true if string == true
132
+ if command == :listall
133
+ build_hash(string)
134
+ elsif command == :listallinfo
135
+ build_response(command, string, [:directory, :playlist])
136
+ else
137
+ build_response(command, string)
138
+ end
131
139
  end
132
140
 
133
141
  # Parses the response into appropriate objects (either a single object,
134
142
  # or an array of objects or an array of hashes).
135
143
  #
136
144
  # @return [Array<Hash>, Array<String>, String, Integer] Parsed response.
137
- def build_response(command, string)
145
+ def build_response(command, string, filter=nil)
138
146
  return [] if string.nil? || !string.is_a?(String)
139
147
 
148
+ # filter lines if filter specified
149
+ string = filter_lines(string,filter) if filter
150
+
140
151
  chunks = make_chunks(string)
141
152
  # if there are any new lines (more than one data piece), it's a hash, else an object.
142
153
  is_hash = chunks.any? {|chunk| chunk.include? "\n"}
143
154
 
144
155
  list = chunks.inject([]) do |result, chunk|
145
- result << (is_hash ? build_hash(chunk) : parse_line(chunk))
156
+ result << (is_hash ? build_hash(chunk) : parse_line(chunk)[1]) # parse_line(chunk)[1] is object
146
157
  end
147
158
 
148
159
  # if list has only one element and not set to explicit array, return it, else return array
@@ -150,19 +161,5 @@ class MPD
150
161
  return result
151
162
  end
152
163
 
153
- # Parse the response into groups that have the same key (used for file lists,
154
- # groups together files, directories and playlists).
155
- # @return [Hash<Array>] A hash of key groups.
156
- def build_grouped_response(string)
157
- return [] if string.nil? || !string.is_a?(String)
158
-
159
- string.split("\n").each_with_object({}) do |line, hash|
160
- key, value = line.split(': ', 2)
161
- key = key.downcase.to_sym
162
- hash[key] ||= []
163
- hash[key] << parse_key(key, value.chomp) # map val to key
164
- end
165
- end
166
-
167
164
  end
168
- end
165
+ end
@@ -85,4 +85,4 @@ class MPD
85
85
  end
86
86
 
87
87
  end
88
- end
88
+ end
@@ -19,7 +19,7 @@ class MPD
19
19
  # @param [Hash] pos :id of the song where to start playing.
20
20
  # @macro returnraise
21
21
  def play(pos = nil)
22
- if pos.is_a?(Hash)
22
+ if pos.is_a?(Hash)
23
23
  if pos[:id]
24
24
  send_command :playid, priority, pos[:id]
25
25
  else
@@ -61,4 +61,4 @@ class MPD
61
61
  end
62
62
  end
63
63
  end
64
- end
64
+ end
@@ -110,7 +110,7 @@ class MPD
110
110
  def songs_by_artist(artist)
111
111
  search :artist, artist
112
112
  end
113
-
113
+
114
114
  end
115
115
  end
116
- end
116
+ end
@@ -146,4 +146,4 @@ class MPD
146
146
  end
147
147
  end
148
148
  end
149
- end
149
+ end
@@ -23,4 +23,4 @@ class MPD
23
23
  end
24
24
  end
25
25
  end
26
- end
26
+ end
@@ -85,4 +85,4 @@ class MPD
85
85
 
86
86
  end
87
87
  end
88
- end
88
+ end
@@ -12,4 +12,4 @@ class MPD
12
12
 
13
13
  end
14
14
  end
15
- end
15
+ end
@@ -43,7 +43,7 @@ class MPD
43
43
  # @param [Hash] pos :id to specify the song ID to delete instead of position.
44
44
  # @macro returnraise
45
45
  def delete(pos)
46
- if pos.is_a?(Hash)
46
+ if pos.is_a?(Hash)
47
47
  if pos[:id]
48
48
  send_command :deleteid, pos[:id]
49
49
  else
@@ -64,7 +64,7 @@ class MPD
64
64
  # @param [Hash] from :id to specify the song ID to move instead of position.
65
65
  # @macro returnraise
66
66
  def move(from, to)
67
- if pos.is_a?(Hash)
67
+ if pos.is_a?(Hash)
68
68
  if pos[:id]
69
69
  send_command :moveid, pos[:id], to
70
70
  else
@@ -110,7 +110,7 @@ class MPD
110
110
  # @param [Range] pos A range of positions.
111
111
  # @param [Hash] pos :id to specify the song ID to move instead of position.
112
112
  def song_priority(priority, pos)
113
- if pos.is_a?(Hash)
113
+ if pos.is_a?(Hash)
114
114
  if pos[:id]
115
115
  send_command :prioid, priority, pos[:id]
116
116
  else
@@ -151,4 +151,4 @@ class MPD
151
151
 
152
152
  end
153
153
  end
154
- end
154
+ end
@@ -43,4 +43,4 @@ class MPD
43
43
  end
44
44
  end
45
45
  end
46
- end
46
+ end
@@ -26,7 +26,7 @@ class MPD
26
26
  # Adds a sticker value to the specified object. If a sticker
27
27
  # item with that name already exists, it is replaced.
28
28
  def set_sticker(type, uri, name, value)
29
- send_command :sticker, :set, type, uri, name
29
+ send_command :sticker, :set, type, uri, name, value
30
30
  end
31
31
 
32
32
  # Deletes a sticker value from the specified object. If you do
@@ -49,4 +49,4 @@ class MPD
49
49
  end
50
50
 
51
51
  end
52
- end
52
+ end
data/lib/ruby-mpd/song.rb CHANGED
@@ -5,11 +5,16 @@ class MPD; end
5
5
  # If the field doesn't exist or isn't set, nil will be returned
6
6
  class MPD::Song
7
7
  # length in seconds
8
- attr_accessor :time
8
+ attr_reader :file, :title, :time, :artist, :album, :albumartist
9
9
 
10
10
  def initialize(options)
11
- @data = {} #allowed fields are @types + :file
11
+ @data = {} # allowed fields are @types + :file
12
12
  @time = options.delete(:time).first #HAXX for array return
13
+ @file = options.delete(:file)
14
+ @title = options.delete(:title)
15
+ @artist = options.delete(:artist)
16
+ @album = options.delete(:album)
17
+ @albumartist = options.delete(:albumartist)
13
18
  @data.merge! options
14
19
  end
15
20
 
@@ -34,4 +39,4 @@ class MPD::Song
34
39
  raise NoMethodError, "#{m}"
35
40
  end
36
41
  end
37
- end
42
+ end
data/ruby-mpd.gemspec CHANGED
@@ -3,7 +3,7 @@
3
3
  Gem::Specification.new do |s|
4
4
  s.platform = Gem::Platform::RUBY
5
5
  s.name = 'ruby-mpd'
6
- s.version = '0.1.7'
6
+ s.version = '0.1.8'
7
7
  s.homepage = 'https://github.com/archSeer/ruby-mpd'
8
8
  s.authors = ["Blaž Hrastnik"]
9
9
  s.email = ['speed.the.bboy@gmail.com']
data/test/libtests.rb CHANGED
@@ -305,7 +305,7 @@ class MPDTester < Test::Unit::TestCase
305
305
  @mpd.connect
306
306
 
307
307
  dirs = @mpd.directories
308
-
308
+
309
309
  assert_equal 7, dirs.size
310
310
  assert_equal 'Astral_Projection', dirs[0]
311
311
  assert_equal 'Astral_Projection/Dancing_Galaxy', dirs[1]
@@ -439,7 +439,7 @@ class MPDTester < Test::Unit::TestCase
439
439
  pls = @mpd.playlist
440
440
 
441
441
  assert_equal 8, pls.size
442
-
442
+
443
443
  pls.each do |song|
444
444
  assert_equal 'Astral Projection', song.artist
445
445
  assert_equal 'Dancing Galaxy', song.album
@@ -581,7 +581,7 @@ class MPDTester < Test::Unit::TestCase
581
581
  @mpd.connect
582
582
 
583
583
  assert @mpd.ping
584
-
584
+
585
585
  @mpd.disconnect
586
586
  assert_raise(MPD::ServerError) {@mpd.ping}
587
587
  end
@@ -911,7 +911,7 @@ class MPDTester < Test::Unit::TestCase
911
911
  assert @mpd.play
912
912
 
913
913
  sleep 2
914
-
914
+
915
915
  assert @mpd.pause = true
916
916
 
917
917
  sleep 2
@@ -923,7 +923,7 @@ class MPDTester < Test::Unit::TestCase
923
923
  song = @mpd.current_song
924
924
 
925
925
  assert_equal 'Flying Into A Star', song.title
926
-
926
+
927
927
  status = @mpd.status
928
928
 
929
929
  assert_equal '200:585', status['time']
@@ -940,7 +940,7 @@ class MPDTester < Test::Unit::TestCase
940
940
  assert @mpd.play
941
941
 
942
942
  sleep 2
943
-
943
+
944
944
  assert @mpd.pause = true
945
945
 
946
946
  sleep 2
@@ -952,7 +952,7 @@ class MPDTester < Test::Unit::TestCase
952
952
  song = @mpd.current_song
953
953
 
954
954
  assert_equal 'Flying Into A Star', song.title
955
-
955
+
956
956
  status = @mpd.status
957
957
 
958
958
  assert_equal '200:585', status['time']
@@ -963,7 +963,7 @@ class MPDTester < Test::Unit::TestCase
963
963
 
964
964
  def test_volume
965
965
  @mpd.connect
966
-
966
+
967
967
  vol = @mpd.volume
968
968
 
969
969
  @mpd.volume = 30
@@ -1121,7 +1121,7 @@ class MPDTester < Test::Unit::TestCase
1121
1121
  assert_equal 1, ret
1122
1122
 
1123
1123
  status = @mpd.status
1124
-
1124
+
1125
1125
  assert_equal '1', status['updating_db']
1126
1126
 
1127
1127
  status = @mpd.status
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-mpd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-22 00:00:00.000000000 Z
12
+ date: 2013-01-27 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: A powerful, modern and feature complete library for the Music Player
15
15
  Daemon.