Project

General

Profile

unexist.dev

subtle

Assorted tidbits and projects

mpd.rb

Dominikh Honnef, 05/20/2011 12:34 AM

 
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.draw_icons       = s.config[:draw_icons].nil? ? true : s.config[:draw_icons]
306
  s.pause_label      = s.config[:pause_label] || '*'
307
  s.show_pause       = s.config[:show_pause] || true
308

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

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

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

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

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

    
338
  s.format_values = {}
339

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

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

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

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

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

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

    
366
  update_status
367
end # }}}
368

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

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

    
383
        # Sanity?
384
        self.format_values.each do |k, v|
385
          puts k
386
          if(self.mpd.current_song.include?(k))
387
            self.format_values[k].value = self.mpd.current_song[k] || "n/a"
388
          elsif k.end_with?('_color')
389
            self.format_values[k].value = self.colors[k.chomp('_color')]
390
          end
391
        end
392

    
393
        # Modes
394
        modes << self.icons[:repeat]   if(self.mpd.repeat)
395
        modes << self.icons[:random]   if(self.mpd.random)
396
        modes << self.icons[:database] if(self.mpd.database)
397
        modes = " %s" % [ modes ] unless(modes.empty?)
398

    
399
        # Assemble format
400
        mesg = self.format_string % self.format_values.values
401
      elsif(:stop == self.mpd.state)
402
        mesg = self.colors['stop'] + self.stop_text
403
        icon = :play
404
      end
405
    end
406

    
407
    if self.draw_icons
408
      self.data = "%s%s%s%s%s %s" % [
409
        self.icons[icon], self.icons[:stop],
410
        self.icons[:prev], self.icons[:next],
411
        modes, mesg
412
      ]
413
    else
414
      if self.show_pause && self.mpd.state == :pause
415
        mesg = "%s%s %s" % [self.colors['pause'], self.pause_label, mesg]
416
      end
417
      self.data = mesg
418
    end
419

    
420
  end # }}}
421

    
422
  def smart_play_command
423
    case self.mpd.state
424
    when :stop  then "play"
425
    when :pause then "pause 0"
426
    when :play  then "pause 1"
427
    end
428
  end
429
end # }}}
430

    
431
on :mouse_down do |s, x, y, b| # {{{
432
  if(s.mpd.socket.nil?)
433
    watch(s.mpd.socket) if(s.mpd.connect)
434
    update_status
435
  else
436
    # Send to socket
437
    if s.draw_icons
438
      action = case b
439
               when 1
440
                 case x
441
                 when 2..17  then smart_play_command
442
                 when 18..33 then "stop"
443
                 when 34..49 then "previous"
444
                 when 50..65 then "next"
445
                 else
446
                   s.def_action == "play" ? smart_play_command : s.def_action
447
                 end
448
               when 4 then s.wheel_up
449
               when 5 then s.wheel_down
450
               end
451
    else
452
      action = case b
453
               when 1
454
                 s.def_action == "play" ? smart_play_command : s.def_action
455
               when 4 then s.wheel_up
456
               when 5 then s.wheel_down
457
               end
458
    end
459
    s.mpd.action(action)
460
  end
461
end # }}}
462

    
463
on :watch do |s| # {{{
464
  unwatch unless(s.mpd.update)
465
  update_status
466
end # }}}