ruby-mpd 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.