Project

General

Profile

unexist.dev

Assorted tidbits and projects

mpd.rb

Douglas Freed, 05/23/2011 05:00 PM

 
1
# Mpd sublet file
2
# Created with sur-0.1
3
require "socket"
4
require "subtle/subtlext"
5

    
6
# Class Pointer {{{
7
class Pointer
8
  attr_accessor :value
9

    
10
  def initialize(value = nil)
11
    @value = value
12
  end
13

    
14
  def to_s
15
    value.to_s
16
  end
17
end # }}}
18

    
19
# Class Mpd {{{
20
class Mpd
21
  # Mpd state
22
  attr_accessor :state
23

    
24
  # Mpd options
25
  attr_accessor :repeat
26
  attr_accessor :random
27
  attr_accessor :database
28

    
29
  # Mpd socket
30
  attr_accessor :socket
31

    
32
  # Mpd current song
33
  attr_accessor :current_song
34

    
35
  ## initialize {{{
36
  # Create a new mpd object
37
  # @param [String]  host      Hostname
38
  # @param [Fixnum]  port      Port
39
  # @param [String]  password  Password
40
  ##
41

    
42
  def initialize(host, port, password = nil)
43
    @host         = host
44
    @port         = port
45
    @password     = password
46
    @socket       = nil
47
    @state        = :off
48
    @repeat       = false
49
    @random       = false
50
    @database     = false
51
    @current_song = {}
52
  end # }}}
53

    
54
  ## connect {{{
55
  # Open connection to mpd
56
  # @return [Bool] Whether connection succeed
57
  ##
58

    
59
  def connect
60
    begin
61
      @socket = TCPSocket.new(@host, @port)
62

    
63
      # Handle SIGPIPE
64
      trap "PIPE" do
65
        @socket = nil
66
        disconnect
67
      end
68

    
69
      # Wait for mpd header
70
      safe_read(1)
71

    
72
      # Send password if any
73
      unless(@password.nil?)
74
        safe_write("password #{@password}")
75
        return false unless(get_ok(1))
76
      end
77

    
78
      parse_status
79
      parse_current
80
      idle
81
    rescue Errno::ECONNREFUSED
82
      puts "mpd not running"
83
    rescue
84
    end
85

    
86
    !@socket.nil?
87
  end # }}}
88

    
89
  ## disconnect {{{
90
  # Send close and shutdown
91
  ###
92

    
93
  def disconnect
94
    safe_write("close") unless(@socket.nil?)
95

    
96
    @socket = nil
97
    @state  = :off
98
  end # }}}
99

    
100
  ## action # {{{
101
  # Send action to mpd
102
  # @param [String]  command  Mpd action
103
  ##
104

    
105
  def action(command)
106
    noidle
107
    safe_write(command)
108
  end # }}}
109

    
110
  ## update {{{
111
  # Update mpd
112
  # @return [Bool] Whether update was successful
113
  ##
114

    
115
  def update
116
    get_ok(1)
117
    parse_status
118
    parse_current
119
    idle
120

    
121
    !@socket.nil?
122
  end # }}}
123

    
124
  private
125

    
126
  ## safe_read {{{
127
  # Read data from socket
128
  # @param [Fixnum]  timeout  Timeout in seconds
129
  # @return [String] Read data
130
  ##
131

    
132
  def safe_read(timeout = 0)
133
    line = ""
134

    
135
    unless(@socket.nil?)
136
      begin
137
        sets = select([ @socket ], nil, nil, timeout)
138
        line = @socket.readline unless(sets.nil?) #< No nil is a socket hit
139
      rescue EOFError
140
        puts "mpd read: EOF"
141
        @socket = nil
142
        disconnect
143
      rescue
144
        disconnect
145
      end
146
    end
147

    
148
    line
149
  end # }}}
150

    
151
  ## safe_write {{{
152
  # Write dats to socket
153
  # @param [String]  str  String to write
154
  ##
155

    
156
  def safe_write(str)
157
    return if(str.nil? or str.empty?)
158

    
159
    unless(@socket.nil?)
160
      begin
161
        @socket.write("%s\n" % [ str ])
162
      rescue
163
        disconnect
164
      end
165
    end
166
  end # }}}
167

    
168
  ## idle {{{
169
  # Send idle command
170
  ##
171

    
172
  def idle
173
    safe_write("idle player options update") unless(@socket.nil?)
174
  end # }}}
175

    
176
  ## noidle {{{
177
  # Send noidle command
178
  ###
179

    
180
  def noidle
181
    safe_write("noidle")
182
    get_ok(1)
183
  end # }}}
184

    
185
  ## get_ok {{{
186
  # Get ok or error
187
  # @param [Fixnum]  timeout  Timeout in seconds
188
  # @return [Bool] Whether mpd return ok
189
  ##
190

    
191
  def get_ok(timeout = 0)
192
    unless(@socket.nil?)
193
      line = safe_read(timeout)
194
      line = safe_read(timeout) if(line.match(/^changed/)) #< Skip changed message
195

    
196
      # Check result
197
      if(line.match(/^OK/))
198
        true
199
      elsif((match = line.match(/^ACK \[(.*)\] \{(.*)\} (.*)/)))
200
        disconnect
201

    
202
        # Probably non-recoverable
203
        puts "mpd %s error: %s" % [ match[2], match[3] ]
204

    
205
        false
206
      end
207
    end
208
  end # }}}
209

    
210
  ## get_reply {{{
211
  # Send command and return reply as hash
212
  # @oaran [String]  command  Command to send
213
  # return [Hash] Data hash
214
  ###
215

    
216
  def get_reply(command)
217
    hash = {}
218

    
219
    unless(@socket.nil?)
220
      begin
221
        safe_write(command)
222

    
223
        while
224
          line = safe_read(1)
225

    
226
          # Check response
227
          if(line.match(/^OK/))
228
            break
229
          elsif((match = line.match(/^ACK \[(.*)\] \{(.*)\} (.*)/)))
230
            disconnect
231

    
232
            # Probably non-recoverable
233
            puts "mpd %s error: %s" % [ match[2], match[3] ]
234

    
235
            raise #< Exit loop
236
          elsif((match = line.match(/^(\w+): (.+)$/)))
237
            hash[match[1].downcase] = match[2]
238
          end
239
        end
240
      rescue
241
        hash = {}
242
      end
243
    end
244

    
245
    hash
246
  end # }}}
247

    
248
  ## parse_status {{{
249
  # Parse mpd status
250
  ###
251

    
252
  def parse_status
253
    unless(@socket.nil?)
254
      status = get_reply("status")
255

    
256
      # Convert state
257
      @state = case status["state"]
258
        when "play"  then :play
259
        when "pause" then :pause
260
        when "stop"  then :stop
261
        else :off
262
      end
263

    
264
      # Set modes
265
      @repeat   = (0 == status["repeat"].to_i) ? false : true
266
      @random   = (0 == status["random"].to_i) ? false : true
267
      @database = !status["updating_db"].nil?
268
    end
269
  end # }}}
270

    
271
  ## parse_current {{{
272
  # Parse mpd current song
273
  ##
274

    
275
  def parse_current
276
    unless(@socket.nil?)
277
      @current_song = get_reply("currentsong")
278
    else
279
      @current_song = {}
280
    end
281
  end # }}}
282
end # }}}
283

    
284
configure :mpd do |s| # {{{
285
  # Icons
286
  s.icons = {
287
    :play     => Subtlext::Icon.new("play.xbm"),
288
    :pause    => Subtlext::Icon.new("pause.xbm"),
289
    :stop     => Subtlext::Icon.new("stop.xbm"),
290
    :prev     => Subtlext::Icon.new("prev.xbm"),
291
    :next     => Subtlext::Icon.new("next.xbm"),
292
    :note     => Subtlext::Icon.new("note.xbm"),
293
    :repeat   => Subtlext::Icon.new("repeat.xbm"),
294
    :random   => Subtlext::Icon.new("shuffle.xbm"),
295
    :database => Subtlext::Icon.new("diskette.xbm")
296
  }
297

    
298
  # Options
299
  s.def_action       = s.config[:def_action]
300
  s.wheel_up         = s.config[:wheel_up]
301
  s.wheel_down       = s.config[:wheel_down]
302
  s.format_string    = s.config[:format_string] || "%note%%artist% - %title%"
303
  s.stop_text        = s.config[:stop_text] || "mpd stopped"
304
  s.not_running_text = s.config[:not_running_text] || "mpd not running"
305
  s.blank_text       = s.config[:blank_text] || "n/a"
306
  s.draw_icons       = s.config[:draw_icons].nil? ? true : s.config[:draw_icons]
307
  s.pause_label      = s.config[:pause_label] || '*'
308
  s.show_pause       = s.config[:show_pause] || true
309

    
310
  ## Colors
311
  s.use_colors = s.config[:use_colors]
312
  colors = %w[artist album title track id pause stop note]
313

    
314
  if s.use_colors
315
    s.colors = {
316
      'artist'  => Subtlext::Color.new(s.config[:artist_color] || '#757575'),
317
      'album'   => Subtlext::Color.new(s.config[:album_color] || '#757575'),
318
      'title'   => Subtlext::Color.new(s.config[:title_color] || '#B8B8B8'),
319
      'track'   => Subtlext::Color.new(s.config[:track_color] || '#757575'),
320
      'id'      => Subtlext::Color.new(s.config[:id_color] || '#757575'),
321
      'pause'   => Subtlext::Color.new(s.config[:pause_color] || '#FECF35'),
322
      'stop'    => Subtlext::Color.new(s.config[:stop_color] || '#757575'),
323
      'note'    => Subtlext::Color.new(s.config[:note_color] || '#ffffff'),
324
    }
325
  else
326
    s.colors = Hash[colors.zip [""] * colors.size]
327
  end
328

    
329
  # Sanitize actions
330
  valid = [ "play", "pause 0", "pause 1", "stop", "previous", "next" ]
331

    
332
  s.def_action = "next"     unless(valid.include?(s.def_action))
333
  s.wheel_up   = "next"     unless(valid.include?(s.wheel_up))
334
  s.wheel_down = "previous" unless(valid.include?(s.wheel_down))
335

    
336
  # Parse format string once
337
  fields = [ "%note%", "%artist%", "%album%", "%title%", "%track%", "%id%" ]
338

    
339
  s.format_values = {}
340

    
341
  s.format_string.gsub!(/%[^%]+%/) do |f|
342
    if(fields.include?(f))
343
      name = f.delete("%")
344

    
345
      # Note: the _color format has to come before the other one
346
      format_values[name + '_color'] = Pointer.new
347
      if("%note%" == f)
348
        format_values[name] = self.icons[:note]
349
      else
350
        format_values[name] = Pointer.new
351
      end
352

    
353
      "%s%s"
354
    else
355
      ""
356
    end
357
  end
358

    
359
  # Create mpd object
360
  host, password = (s.config[:host] || ENV["MPD_HOST"] || "localhost").split("@")
361
  port           = s.config[:port]  || ENV["MPD_PORT"] || 6600
362

    
363
  s.mpd = Mpd.new(host, port, password)
364

    
365
  watch(s.mpd.socket) if(s.mpd.connect)
366

    
367
  update_status
368
end # }}}
369

    
370
helper do |s| # {{{
371
  def update_status # {{{
372
    mesg  = self.not_running_text
373
    modes = ""
374
    icon  = :play
375

    
376
    unless(self.mpd.socket.nil?)
377
      if(:play == self.mpd.state or :pause == self.mpd.state)
378
        # Select icon
379
        icon = case self.mpd.state
380
               when :play  then :pause
381
               when :pause then :play
382
               end
383

    
384
        # Sanity?
385
        self.format_values.each do |k, v|
386
          if(self.mpd.current_song.include?(k))
387
            self.format_values[k].value = if self.mpd.current_song[k].nil? 
388
                                          then self.blank_text
389
                                          else self.mpd.current_song[k]
390
                                          end
391
          elsif k.end_with?('_color')
392
            self.format_values[k].value = self.colors[k.chomp('_color')]
393
          else
394
            self.format_values[k].value = self.blank_text
395
          end
396
        end
397

    
398
        # Modes
399
        modes << self.icons[:repeat]   if(self.mpd.repeat)
400
        modes << self.icons[:random]   if(self.mpd.random)
401
        modes << self.icons[:database] if(self.mpd.database)
402
        modes = " %s" % [ modes ] unless(modes.empty?)
403

    
404
        # Assemble format
405
        mesg = self.format_string % self.format_values.values
406
      elsif(:stop == self.mpd.state)
407
        mesg = self.colors['stop'] + self.stop_text
408
        icon = :play
409
      end
410
    end
411

    
412
    if self.draw_icons
413
      self.data = "%s%s%s%s%s %s" % [
414
        self.icons[icon], self.icons[:stop],
415
        self.icons[:prev], self.icons[:next],
416
        modes, mesg
417
      ]
418
    else
419
      if self.show_pause && self.mpd.state == :pause
420
        mesg = "%s%s %s" % [self.colors['pause'], self.pause_label, mesg]
421
      end
422
      self.data = mesg
423
    end
424

    
425
  end # }}}
426

    
427
  def smart_play_command
428
    case self.mpd.state
429
    when :stop  then "play"
430
    when :pause then "pause 0"
431
    when :play  then "pause 1"
432
    end
433
  end
434
end # }}}
435

    
436
on :mouse_down do |s, x, y, b| # {{{
437
  if(s.mpd.socket.nil?)
438
    watch(s.mpd.socket) if(s.mpd.connect)
439
    update_status
440
  else
441
    # Send to socket
442
    if s.draw_icons
443
      action = case b
444
               when 1
445
                 case x
446
                 when 0..15  then smart_play_command
447
                 when 16..31 then "stop"
448
                 when 32..47 then "previous"
449
                 when 48..63 then "next"
450
                 else
451
                   s.def_action == "play" ? smart_play_command : s.def_action
452
                 end
453
               when 4 then s.wheel_up
454
               when 5 then s.wheel_down
455
               end
456
    else
457
      action = case b
458
               when 1
459
                 s.def_action == "play" ? smart_play_command : s.def_action
460
               when 4 then s.wheel_up
461
               when 5 then s.wheel_down
462
               end
463
    end
464
    s.mpd.action(action)
465
  end
466
end # }}}
467

    
468
on :watch do |s| # {{{
469
  unwatch unless(s.mpd.update)
470
  update_status
471
end # }}}