librmpd 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/librmpd.rb ADDED
@@ -0,0 +1,1182 @@
1
+ #
2
+ #== librmpd.rb
3
+ #
4
+ # librmpd.rb is another Ruby MPD Library with a goal of greater
5
+ # ease of use, more functionality, and thread safety
6
+ #
7
+ # Author:: Andrew Rader (bitwise_mcgee AT yahoo.com | http://nymb.us)
8
+ # Copyright:: Copyright (c) 2006 Andrew Rader
9
+ # License:: Distributed under the GNU GPL v2 (See COPYING file)
10
+ #
11
+ # This was written with MPD version 0.11.5 (http://www.musicpd.org)
12
+ #
13
+ # The main class is the MPD class. This provides the functionality for
14
+ # talking to the server as well as setting up callbacks for when events
15
+ # occur (such as song changes, state changes, etc). The use of callbacks
16
+ # is optional, if they are used a seperate thread will continuously poll
17
+ # the server on its status, when something is changed, your program will
18
+ # be notified via any callbacks you have set. Most methods are the same
19
+ # as specified in the MPD Server Protocol, however some have been modified
20
+ # or renamed. Most notable is the list* and lsinfo functions have been
21
+ # replace with more sane methods (such as `files` for all files)
22
+ #
23
+ #== Usage
24
+ #
25
+ # First create an MPD object
26
+ #
27
+ # require 'rubygems'
28
+ # require 'librmpd'
29
+ #
30
+ # mpd = MPD.new 'localhost', 6600
31
+ #
32
+ # and connect it to the server
33
+ #
34
+ # mpd.connect
35
+ #
36
+ # You can now issue any of the commands. Each command is documented below.
37
+ #
38
+ #=== Callbacks
39
+ #
40
+ # Callbacks are a way to easily setup your client as event based, rather
41
+ # than polling based. This means rather than having to check for changes
42
+ # in the server, you setup a few methods that will be called when those
43
+ # changes occur. For example, you could have a 'state_changed' method
44
+ # that will be called whenever the server changes state. You could then
45
+ # have this method change a label to reflect to the new state.
46
+ #
47
+ # To use callbacks in your program, first setup your callback methods. For
48
+ # example, say you have the class MyClient. Simply define whatever
49
+ # callbacks you want inside your class. See the documentation on the
50
+ # callback type constants in the MPD class for details on how each callback
51
+ # is called
52
+ #
53
+ # Once you have your callback methods defined, use the register_callback
54
+ # methods to inform librmpd about them. You can have multiple callbacks
55
+ # for each type of callback without problems. Simply use object.method('method_name')
56
+ # to get a reference to a Method object. Pass this object to the
57
+ # register_callback (along with the proper type value), and you're set.
58
+ #
59
+ # An Example:
60
+ #
61
+ # class MyClient
62
+ # ...
63
+ # def state_callback( newstate )
64
+ # puts "MPD Changed State: #{newstate}"
65
+ # end
66
+ # ...
67
+ # end
68
+ #
69
+ # client = MyClient.new
70
+ # mpd = MPD.new
71
+ # mpd.register_callback(client.method('state_callback'), MPD::STATE_CALLBACK)
72
+ #
73
+ # # Connect and Enable Callbacks
74
+ # mpd.connect( true )
75
+ #
76
+ # In order for the callback to be used, you must enable callbacks when you
77
+ # connect by passing true to the connect method. Now, whenever the state changes
78
+ # on the server, myclientobj's state_callback method will be called (and passed
79
+ # the new state as an argument)
80
+
81
+ class MPD
82
+
83
+ require 'socket'
84
+ require 'thread'
85
+
86
+ #
87
+ # These are the callback types used in registering callbacks
88
+
89
+ # STATE_CALLBACK: This is used to listen for changes in the server state
90
+ #
91
+ # The callback will always be called with a single string argument
92
+ # which may an empty string.
93
+ STATE_CALLBACK = 0
94
+
95
+ # CURRENT_SONG_CALLBACK: This is used to listen for changes in the current
96
+ # song being played by the server.
97
+ #
98
+ # The callback will always be called with a single argument, an MPD::Song
99
+ # object, or, if there were problems, nil
100
+ CURRENT_SONG_CALLBACK = 1
101
+
102
+ # PLAYLIST_CALLBACK: This is used to listen for when changes in the playlist
103
+ # are made.
104
+ #
105
+ # The callback will always be called with a single argument, an integer
106
+ # value for the current playlist or 0 if there were problems
107
+ PLAYLIST_CALLBACK = 2
108
+
109
+ # TIME_CALLBACK: This is used to listen for when the playback time changes
110
+ #
111
+ # The callback will always be called with two arguments. The first is
112
+ # the integer number of seconds elapsed (or 0 if errors), the second is
113
+ # the total number of seconds in the song (or 0 if errors)
114
+ TIME_CALLBACK = 3
115
+
116
+ # VOLUME_CALLBACK: This is used to listen for when the volume changes
117
+ #
118
+ # The callback will always be called with a single argument, an integer
119
+ # value of the volume (or 0 on errors)
120
+ VOLUME_CALLBACK = 4
121
+
122
+ # REPEAT_CALLBACK: This is used to listen for changes to the repeat flag
123
+ #
124
+ # The callback will always be called with a single argument, a boolean
125
+ # true or false depending on if the repeat flag is set / unset
126
+ REPEAT_CALLBACK = 5
127
+
128
+ # RANDOM_CALLBACK: This is used to listen for changed to the random flag
129
+ #
130
+ # The callback will always be called with a single argument, a boolean
131
+ # true or false depending on if the random flag is set / unset
132
+ RANDOM_CALLBACK = 6
133
+
134
+ # PLAYLIST_LENGTH_CALLBACK: This is used to listen for changes to the
135
+ # playlist length
136
+ #
137
+ # The callback will always be called with a single argument, an integer
138
+ # value of the current playlist's length (or 0 on errors)
139
+ PLAYLIST_LENGTH_CALLBACK = 7
140
+
141
+ # CROSSFADE_CALLBACK: This is used to listen for changes in the crossfade
142
+ # setting
143
+ #
144
+ # The callback will always be called with a single argument, an integer
145
+ # value of the number of seconds the crossfade is set to (or 0 on errsors)
146
+ CROSSFADE_CALLBACK = 8
147
+
148
+ # CURRENT_SONGID_CALLBACK: This is used to listen for changes in the
149
+ # current song's songid
150
+ #
151
+ # The callback will always be called with a single argument, an integer
152
+ # value of the id of the current song (or 0 on errors)
153
+ CURRENT_SONGID_CALLBACK = 9
154
+
155
+ # BITRATE_CALLBACK: This is used to listen for changes in the playback
156
+ # bitrate
157
+ #
158
+ # The callback will always be called with a single argument, an integer
159
+ # value of the bitrate of the playback (or 0 on errors)
160
+ BITRATE_CALLBACK = 10
161
+
162
+ # AUDIO_CALLBACK: This is used to listen for changes in the audio
163
+ # quality data (sample rate etc)
164
+ #
165
+ # The callback will always be called with three arguments, first,
166
+ # an integer holding the sample rate (or 0 on errors), next an
167
+ # integer holding the number of bits (or 0 on errors), finally an
168
+ # integer holding the number of channels (or 0 on errors)
169
+ AUDIO_CALLBACK = 11
170
+
171
+ # CONNECTION_CALLBACK: This is used to listen for changes in the
172
+ # connection to the server
173
+ #
174
+ # The callback will always be called with a single argument,
175
+ # a boolean true if the client is now connected to the server,
176
+ # and a boolean false if it has been disconnected
177
+ CONNECTION_CALLBACK = 12
178
+
179
+ #
180
+ #== Song
181
+ #
182
+ # This class is a glorified Hash used to represent a song
183
+ # It provides several reader methods for accessing common tags of a song,
184
+ # If a tag doesn't have a reader method, just use the normal hash method of
185
+ # accessing it
186
+ #
187
+ class Song < Hash
188
+ def songid
189
+ self['id']
190
+ end
191
+
192
+ def pos
193
+ self['pos']
194
+ end
195
+
196
+ def artist
197
+ self['artist']
198
+ end
199
+
200
+ def album
201
+ self['album']
202
+ end
203
+
204
+ def title
205
+ self['title']
206
+ end
207
+
208
+ def track
209
+ self['track']
210
+ end
211
+
212
+ def name
213
+ self['name']
214
+ end
215
+
216
+ def genre
217
+ self['genre']
218
+ end
219
+
220
+ def date
221
+ self['date']
222
+ end
223
+
224
+ def composer
225
+ self['composer']
226
+ end
227
+
228
+ def performer
229
+ self['performer']
230
+ end
231
+
232
+ def comment
233
+ self['comment']
234
+ end
235
+
236
+ def disc
237
+ self['disc']
238
+ end
239
+
240
+ def file
241
+ self['file']
242
+ end
243
+ end
244
+
245
+ # Initialize an MPD object with the specified hostname and port
246
+ # When called without arguments, 'localhost' and 6600 are used
247
+ def initialize( hostname = 'localhost', port = 6600 )
248
+ @hostname = hostname
249
+ @port = port
250
+ @socket = nil
251
+ @stop_cb_thread = false
252
+ @mutex = Mutex.new
253
+ @cb_thread = nil
254
+ @callbacks = []
255
+ @callbacks[STATE_CALLBACK] = []
256
+ @callbacks[CURRENT_SONG_CALLBACK] = []
257
+ @callbacks[PLAYLIST_CALLBACK] = []
258
+ @callbacks[TIME_CALLBACK] = []
259
+ @callbacks[VOLUME_CALLBACK] = []
260
+ @callbacks[REPEAT_CALLBACK] = []
261
+ @callbacks[RANDOM_CALLBACK] = []
262
+ @callbacks[PLAYLIST_LENGTH_CALLBACK] = []
263
+ @callbacks[CROSSFADE_CALLBACK] = []
264
+ @callbacks[CURRENT_SONGID_CALLBACK] = []
265
+ @callbacks[BITRATE_CALLBACK] = []
266
+ @callbacks[AUDIO_CALLBACK] = []
267
+ @callbacks[CONNECTION_CALLBACK] = []
268
+ end
269
+
270
+ def register_callback( method, type )
271
+ @callbacks[type].push method
272
+ end
273
+
274
+ #
275
+ # Connect to the daemon
276
+ # When called without any arguments, this will just
277
+ # connect to the server and wait for your commands
278
+ # When called with true as an argument, this will
279
+ # enable callbacks by starting a seperate polling thread.
280
+ # This polling thread will also automatically reconnect
281
+ # If is disconnected for whatever reason.
282
+ #
283
+ # connect will return OK plus the version string
284
+ # if successful, otherwise an error will be raised
285
+ #
286
+ # If connect is called on an already connected instance,
287
+ # a RuntimeError is raised
288
+ def connect( callbacks = false )
289
+ if self.connected?
290
+ raise 'MPD Error: Already Connected'
291
+ end
292
+
293
+ @socket = TCPSocket::new @hostname, @port
294
+ ret = @socket.gets # Read the version
295
+
296
+ if callbacks and (@cb_thread.nil? or !@cb_thread.alive?)
297
+ @stop_cb_thread = false
298
+ @cb_thread = Thread.new( self ) { |mpd|
299
+ old_status = {}
300
+ song = ''
301
+ connected = ''
302
+ while !@stop_cb_thread
303
+ begin
304
+ status = mpd.status
305
+ rescue
306
+ status = {}
307
+ end
308
+
309
+ begin
310
+ c = mpd.connected?
311
+ rescue
312
+ c = false
313
+ end
314
+
315
+ if connected != c
316
+ connected = c
317
+ for cb in @callbacks[CONNECTION_CALLBACK]
318
+ cb.call connected
319
+ end
320
+ end
321
+
322
+ if old_status['time'] != status['time']
323
+ if old_status['time'].nil? or old_status['time'].empty?
324
+ old_status['time'] = '0:0'
325
+ end
326
+ t = old_status['time'].split ':'
327
+ elapsed = t[0].to_i
328
+ total = t[1].to_i
329
+ for cb in @callbacks[TIME_CALLBACK]
330
+ cb.call elapsed, total
331
+ end
332
+ end
333
+
334
+ if old_status['volume'] != status['volume']
335
+ for cb in @callbacks[VOLUME_CALLBACK]
336
+ cb.call status['volume'].to_i
337
+ end
338
+ end
339
+
340
+ if old_status['repeat'] != status['repeat']
341
+ for cb in @callbacks[REPEAT_CALLBACK]
342
+ cb.call(status['repeat'] == '1')
343
+ end
344
+ end
345
+
346
+ if old_status['random'] != status['random']
347
+ for cb in @callbacks[RANDOM_CALLBACK]
348
+ cb.call(status['random'] == '1')
349
+ end
350
+ end
351
+
352
+ if old_status['playlist'] != status['playlist']
353
+ for cb in @callbacks[PLAYLIST_CALLBACK]
354
+ cb.call status['playlist'].to_i
355
+ end
356
+ end
357
+
358
+ if old_status['playlistlength'] != status['playlistlength']
359
+ for cb in @callbacks[PLAYLIST_LENGTH_CALLBACK]
360
+ cb.call status['playlistlength'].to_i
361
+ end
362
+ end
363
+
364
+ if old_status['xfade'] != status['xfade']
365
+ for cb in @callbacks[CROSSFADE_CALLBACK]
366
+ cb.call status['xfade'].to_i
367
+ end
368
+ end
369
+
370
+ if old_status['state'] != status['state']
371
+ state = (status['state'].nil? ? '' : status['state'])
372
+ for cb in @callbacks[STATE_CALLBACK]
373
+ cb.call state
374
+ end
375
+ end
376
+
377
+ begin
378
+ s = mpd.current_song
379
+ rescue
380
+ s = nil
381
+ end
382
+
383
+ if song != s
384
+ song = s
385
+ for cb in @callbacks[CURRENT_SONG_CALLBACK]
386
+ cb.call song
387
+ end
388
+ end
389
+
390
+ if old_status['songid'] != status['songid']
391
+ for cb in @callbacks[CURRENT_SONGID_CALLBACK]
392
+ cb.call status['songid'].to_i
393
+ end
394
+ end
395
+
396
+ if old_status['bitrate'] != status['bitrate']
397
+ for cb in @callbacks[BITRATE_CALLBACK]
398
+ cb.call status['bitrate'].to_i
399
+ end
400
+ end
401
+
402
+ if old_status['audio'] != status['audio']
403
+ audio = (status['audio'].nil? ? '0:0:0' : status['audio'])
404
+ a = audio.split ':'
405
+ samp = a[0].to_i
406
+ bits = a[1].to_i
407
+ chans = a[2].to_i
408
+ for cb in @callbacks[AUDIO_CALLBACK]
409
+ cb.call samp, bits, chans
410
+ end
411
+ end
412
+
413
+ old_status = status
414
+ sleep 0.1
415
+
416
+ if !connected
417
+ sleep 2
418
+ begin
419
+ mpd.connect unless @stop_cb_thread
420
+ rescue
421
+ end
422
+ end
423
+ end
424
+ }
425
+ end
426
+
427
+ return ret
428
+ end
429
+
430
+ #
431
+ # Check if the client is connected
432
+ #
433
+ # This will return true only if the server responds
434
+ # otherwise false is returned
435
+ def connected?
436
+ return false if @socket.nil?
437
+ begin
438
+ ret = send_command 'ping'
439
+ rescue
440
+ ret = false
441
+ end
442
+
443
+ return ret
444
+ end
445
+
446
+ #
447
+ # Disconnect from the server. This has no effect
448
+ # if the client is not connected. Reconnect using
449
+ # the connect method. This will also stop the
450
+ # callback thread, thus disabling callbacks
451
+ def disconnect
452
+ @stop_cb_thread = true
453
+
454
+ return if @socket.nil?
455
+
456
+ @socket.puts 'close'
457
+ @socket.close
458
+ @socket = nil
459
+ end
460
+
461
+ #
462
+ # Add the file _path_ to the playlist. If path is a
463
+ # directory, it will be added recursively.
464
+ #
465
+ # Returns true if this was successful,
466
+ # Raises a RuntimeError if the command failed
467
+ def add( path )
468
+ send_command "add \"#{path}\""
469
+ end
470
+
471
+ #
472
+ # Clears the current playlist
473
+ #
474
+ # Returns true if this was successful,
475
+ # Raises a RuntimeError if the command failed
476
+ def clear
477
+ send_command 'clear'
478
+ end
479
+
480
+ #
481
+ # Clears the current error message reported in status
482
+ # ( This is also accomplished by any command that starts playback )
483
+ #
484
+ # Returns true if this was successful,
485
+ # Raises a RuntimeError if the command failed
486
+ def clearerror
487
+ send_command 'clearerror'
488
+ end
489
+
490
+ #
491
+ # Set the crossfade between songs in seconds
492
+ #
493
+ # Raises a RuntimeError if the command failed
494
+ def crossfade=( seconds )
495
+ send_command "crossfade #{seconds}"
496
+ end
497
+
498
+ #
499
+ # Read the crossfade between songs in seconds,
500
+ # Raises a RuntimeError if the command failed
501
+ def crossfade
502
+ status = self.status
503
+ return if status.nil?
504
+ return status['xfade'].to_i
505
+ end
506
+
507
+ #
508
+ # Read the current song in seconds
509
+ #
510
+ # Returns a Song object with the current song's data,
511
+ # Raises a RuntimeError if the command failed
512
+ def current_song
513
+ build_song( send_command('currentsong') )
514
+ end
515
+
516
+ #
517
+ # Delete the song from the playlist, where pos
518
+ # is the song's position in the playlist
519
+ #
520
+ # Returns true if successful,
521
+ # Raises a RuntimeError if the command failed
522
+ def delete( pos )
523
+ send_command "delete #{pos}"
524
+ end
525
+
526
+ #
527
+ # Delete the song with the songid from the playlist
528
+ #
529
+ # Returns true if successful,
530
+ # Raises a RuntimeError if the command failed
531
+ def deleteid( songid )
532
+ send_command "deleteid #{songid}"
533
+ end
534
+
535
+ #
536
+ # Finds songs in the database that are EXACTLY
537
+ # matched by the what argument. type should be
538
+ # 'album', 'artist', or 'title'
539
+ #
540
+ # This returns an Array of MPD::Songs,
541
+ # Raises a RuntimeError if the command failed
542
+ def find( type, what )
543
+ response = send_command "find \"#{type}\" \"#{what}\""
544
+ build_songs_list response
545
+ end
546
+
547
+ #
548
+ # Kills MPD
549
+ #
550
+ # Returns true if successful.
551
+ # Raises a RuntimeError if the command failed
552
+ def kill
553
+ send_command 'kill'
554
+ end
555
+
556
+ #
557
+ # Lists all of the albums in the database
558
+ # The optional argument is for specifying an
559
+ # artist to list the albums for
560
+ #
561
+ # Returns an Array of Album names (Strings),
562
+ # Raises a RuntimeError if the command failed
563
+ def albums( artist = nil )
564
+ list 'album', artist
565
+ end
566
+
567
+ #
568
+ # Lists all of the artists in the database
569
+ #
570
+ # Returns an Array of Artist names (Strings),
571
+ # Raises a RuntimeError if the command failed
572
+ def artists
573
+ list 'artist'
574
+ end
575
+
576
+ #
577
+ # This is used by the albums and artists methods
578
+ # type should be 'album' or 'artist'. If type is 'album'
579
+ # then arg can be a specific artist to list the albums for
580
+ #
581
+ # Returns an Array of Strings,
582
+ # Raises a RuntimeError if the command failed
583
+ def list( type, arg = nil )
584
+ if not arg.nil?
585
+ response = send_command "list #{type} \"#{arg}\""
586
+ else
587
+ response = send_command "list #{type}"
588
+ end
589
+
590
+ list = []
591
+ if not response.nil? and response.kind_of? String
592
+ lines = response.split "\n"
593
+ re = Regexp.new "\\A#{type}: ", 'i'
594
+ for line in lines
595
+ list << line.gsub( re, '' )
596
+ end
597
+ end
598
+
599
+ return list
600
+ end
601
+
602
+ #
603
+ # List all of the directories in the database, starting at path.
604
+ # If path isn't specified, the root of the database is used
605
+ #
606
+ # Returns an Array of directory names (Strings),
607
+ # Raises a RuntimeError if the command failed
608
+ def directories( path = nil )
609
+ if not path.nil?
610
+ response = send_command "listall \"#{path}\""
611
+ else
612
+ response = send_command 'listall'
613
+ end
614
+
615
+ filter_response response, /\Adirectory: /i
616
+ end
617
+
618
+ #
619
+ # List all of the files in the database, starting at path.
620
+ # If path isn't specified, the root of the database is used
621
+ #
622
+ # Returns an Array of file names (Strings).
623
+ # Raises a RuntimeError if the command failed
624
+ def files( path = nil )
625
+ if not path.nil?
626
+ response = send_command "listall \"#{path}\""
627
+ else
628
+ response = send_command 'listall'
629
+ end
630
+
631
+ filter_response response, /\Afile: /i
632
+ end
633
+
634
+ #
635
+ # List all of the playlists in the database
636
+ #
637
+ # Returns an Array of playlist names (Strings)
638
+ def playlists
639
+ response = send_command 'lsinfo'
640
+
641
+ filter_response response, /\Aplaylist: /i
642
+ end
643
+
644
+ #
645
+ # List all of the songs in the database starting at path.
646
+ # If path isn't specified, the root of the database is used
647
+ #
648
+ # Returns an Array of MPD::Songs,
649
+ # Raises a RuntimeError if the command failed
650
+ def songs( path = nil )
651
+ if not path.nil?
652
+ response = send_command "listallinfo \"#{path}\""
653
+ else
654
+ response = send_command 'listallinfo'
655
+ end
656
+
657
+ build_songs_list response
658
+ end
659
+
660
+ #
661
+ # List all of the songs by an artist
662
+ #
663
+ # Returns an Array of MPD::Songs by the artist `artist`,
664
+ # Raises a RuntimeError if the command failed
665
+ def songs_by_artist( artist )
666
+ all_songs = self.songs
667
+ artist_songs = []
668
+ all_songs.each do |song|
669
+ if song.artist == artist
670
+ artist_songs << song
671
+ end
672
+ end
673
+
674
+ return artist_songs
675
+ end
676
+
677
+ #
678
+ # Loads the playlist name.m3u (do not pass the m3u extension
679
+ # when calling) from the playlist directory. Use `playlists`
680
+ # to what playlists are available
681
+ #
682
+ # Returns true if successful,
683
+ # Raises a RuntimeError if the command failed
684
+ def load( name )
685
+ send_command "load \"#{name}\""
686
+ end
687
+
688
+ #
689
+ # Move the song at `from` to `to` in the playlist
690
+ #
691
+ # Returns true if successful,
692
+ # Raises a RuntimeError if the command failed
693
+ def move( from, to )
694
+ send_command "move #{from} #{to}"
695
+ end
696
+
697
+ #
698
+ # Move the song with the `songid` to `to` in the playlist
699
+ #
700
+ # Returns true if successful,
701
+ # Raises a RuntimeError if the command failed
702
+ def moveid( songid, to )
703
+ send_command "moveid #{songid} #{to}"
704
+ end
705
+
706
+ #
707
+ # Plays the next song in the playlist
708
+ #
709
+ # Returns true if successful,
710
+ # Raises a RuntimeError if the command failed
711
+ def next
712
+ send_command 'next'
713
+ end
714
+
715
+ #
716
+ # Set / Unset paused playback
717
+ #
718
+ # Returns true if successful,
719
+ # Raises a RuntimeError if the command failed
720
+ def pause=( toggle )
721
+ send_command 'pause ' + (toggle ? '1' : '0')
722
+ end
723
+
724
+ #
725
+ # Returns true if MPD is paused,
726
+ # Raises a RuntimeError if the command failed
727
+ def paused?
728
+ status = self.status
729
+ return false if status.nil?
730
+ return status['state'] == 'pause'
731
+ end
732
+
733
+ #
734
+ # This is used for authentication with the server
735
+ # `pass` is simply the plaintext password
736
+ #
737
+ # Raises a RuntimeError if the command failed
738
+ def password( pass )
739
+ send_command "password \"#{pass}\""
740
+ end
741
+
742
+ #
743
+ # Ping the server
744
+ #
745
+ # Returns true if successful,
746
+ # Raises a RuntimeError if the command failed
747
+ def ping
748
+ send_command 'ping'
749
+ end
750
+
751
+ #
752
+ # Begin playing the playist. Optionally
753
+ # specify the pos to start on
754
+ #
755
+ # Returns true if successful,
756
+ # Raises a RuntimeError if the command failed
757
+ def play( pos = nil )
758
+ if pos.nil?
759
+ return send_command('play')
760
+ else
761
+ return send_command("play #{pos}")
762
+ end
763
+ end
764
+
765
+ #
766
+ # Returns true if the server's state is set to 'play',
767
+ # Raises a RuntimeError if the command failed
768
+ def playing?
769
+ state = self.status['state']
770
+ return state == 'play'
771
+ end
772
+
773
+ #
774
+ # Begin playing the playlist. Optionally
775
+ # specify the songid to start on
776
+ #
777
+ # Returns true if successful,
778
+ # Raises a RuntimeError if the command failed
779
+ def playid( songid = nil )
780
+ if not songid.nil?
781
+ return(send_command("playid #{songid}"))
782
+ else
783
+ return(send_command('playid'))
784
+ end
785
+ end
786
+
787
+ #
788
+ # Returns the current playlist version number,
789
+ # Raises a RuntimeError if the command failed
790
+ def playlist_version
791
+ self.status['playlist'].to_i
792
+ end
793
+
794
+ #
795
+ # List the current playlist
796
+ # This is the same as playlistinfo w/o args
797
+ #
798
+ # Returns an Array of MPD::Songs,
799
+ # Raises a RuntimeError if the command failed
800
+ def playlist
801
+ response = send_command 'playlistinfo'
802
+ build_songs_list response
803
+ end
804
+
805
+ #
806
+ # Returns the MPD::Song at the position `pos` in the playlist,
807
+ # Raises a RuntimeError if the command failed
808
+ def song_at_pos( pos )
809
+ build_song( send_command("playlistinfo #{pos}") )
810
+ end
811
+
812
+ #
813
+ # Returns the MPD::Song with the `songid` in the playlist,
814
+ # Raises a RuntimeError if the command failed
815
+ def song_with_id( songid )
816
+ build_song( send_command("playlistid #{songid}") )
817
+ end
818
+
819
+ #
820
+ # List the changes since the specified version in the playlist
821
+ #
822
+ # Returns an Array of MPD::Songs,
823
+ # Raises a RuntimeError if the command failed
824
+ def playlist_changes( version )
825
+ response = send_command "plchanges #{version}"
826
+ build_songs_list response
827
+ end
828
+
829
+ #
830
+ # Plays the previous song in the playlist
831
+ #
832
+ # Returns true if successful,
833
+ # Raises a RuntimeError if the command failed
834
+ def previous
835
+ send_command 'previous'
836
+ end
837
+
838
+ #
839
+ # Enable / Disable random playback,
840
+ # Raises a RuntimeError if the command failed
841
+ def random=( toggle )
842
+ send_command 'random ' + (toggle ? '1' : '0')
843
+ end
844
+
845
+ #
846
+ # Returns true if random playback is currently enabled,
847
+ # Raises a RuntimeError if the command failed
848
+ def random?
849
+ rand = self.status['random']
850
+ return rand == '1'
851
+ end
852
+
853
+ #
854
+ # Enable / Disable repeat,
855
+ # Raises a RuntimeError if the command failed
856
+ def repeat=( toggle )
857
+ send_command 'repeat ' + (toggle ? '1' : '0')
858
+ end
859
+
860
+ #
861
+ # Returns true if repeat is enabled,
862
+ # Raises a RuntimeError if the command failed
863
+ def repeat?
864
+ repeat = self.status['repeat']
865
+ return repeat == '1'
866
+ end
867
+
868
+ #
869
+ # Removes (PERMANENTLY!) the playlist `playlist.m3u` from
870
+ # the playlist directory
871
+ #
872
+ # Returns true if successful,
873
+ # Raises a RuntimeError if the command failed
874
+ def rm( playlist )
875
+ send_command "rm \"#{playlist}\""
876
+ end
877
+
878
+ #
879
+ # An Alias for rm
880
+ def remove_playlist( playlist )
881
+ rm playlist
882
+ end
883
+
884
+ #
885
+ # Saves the current playlist to `playlist`.m3u in the
886
+ # playlist directory
887
+ #
888
+ # Returns true if successful,
889
+ # Raises a RuntimeError if the command failed
890
+ def save( playlist )
891
+ send_command "save \"#{playlist}\""
892
+ end
893
+
894
+ #
895
+ # Searches for any song that contains `what` in the `type` field
896
+ # `type` can be 'title', 'artist', 'album' or 'filename'
897
+ # Searches are NOT case sensitive
898
+ #
899
+ # Returns an Array of MPD::Songs,
900
+ # Raises a RuntimeError if the command failed
901
+ def search( type, what )
902
+ build_songs_list( send_command("search #{type} \"#{what}\"") )
903
+ end
904
+
905
+ #
906
+ # Seeks to the position `time` (in seconds) of the
907
+ # song at `pos` in the playlist
908
+ #
909
+ # Returns true if successful,
910
+ # Raises a RuntimeError if the command failed
911
+ def seek( pos, time )
912
+ send_command "seek #{pos} #{time}"
913
+ end
914
+
915
+ #
916
+ # Seeks to the position `time` (in seconds) of the song with
917
+ # the id `songid`
918
+ #
919
+ # Returns true if successful,
920
+ # Raises a RuntimeError if the command failed
921
+ def seekid( songid, time )
922
+ send_command "seekid #{songid} #{time}"
923
+ end
924
+
925
+ #
926
+ # Set the volume
927
+ # The argument `vol` will automatically be bounded to 0 - 100
928
+ #
929
+ # Raises a RuntimeError if the command failed
930
+ def volume=( vol )
931
+ send_command "setvol #{vol}"
932
+ end
933
+
934
+ #
935
+ # Returns the volume,
936
+ # Raises a RuntimeError if the command failed
937
+ def volume
938
+ status = self.status
939
+ return if status.nil?
940
+ return status['volume'].to_i
941
+ end
942
+
943
+ #
944
+ # Shuffles the playlist,
945
+ # Raises a RuntimeError if the command failed
946
+ def shuffle
947
+ send_command 'shuffle'
948
+ end
949
+
950
+ #
951
+ # Returns a Hash of MPD's stats,
952
+ # Raises a RuntimeError if the command failed
953
+ def stats
954
+ response = send_command 'stats'
955
+ build_hash response
956
+ end
957
+
958
+ #
959
+ # Returns a Hash of the current status,
960
+ # Raises a RuntimeError if the command failed
961
+ def status
962
+ response = send_command 'status'
963
+ build_hash response
964
+ end
965
+
966
+ #
967
+ # Stop playing
968
+ #
969
+ # Returns true if successful,
970
+ # Raises a RuntimeError if the command failed
971
+ def stop
972
+ send_command 'stop'
973
+ end
974
+
975
+ #
976
+ # Returns true if the server's state is 'stop',
977
+ # Raises a RuntimeError if the command failed
978
+ def stopped?
979
+ status = self.status
980
+ return false if status.nil?
981
+ return status['state'] == 'stop'
982
+ end
983
+
984
+ #
985
+ # Swaps the song at position `posA` with the song
986
+ # as position `posB` in the playlist
987
+ #
988
+ # Returns true if successful,
989
+ # Raises a RuntimeError if the command failed
990
+ def swap( posA, posB )
991
+ send_command "swap #{posA} #{posB}"
992
+ end
993
+
994
+ #
995
+ # Swaps the song with the id `songidA` with the song
996
+ # with the id `songidB`
997
+ #
998
+ # Returns true if successful,
999
+ # Raises a RuntimeError if the command failed
1000
+ def swapid( songidA, songidB )
1001
+ send_command "swapid #{songidA} #{songidB}"
1002
+ end
1003
+
1004
+ #
1005
+ # Tell the server to update the database. Optionally,
1006
+ # specify the path to update
1007
+ def update( path = nil )
1008
+ ret = ''
1009
+ if not path.nil?
1010
+ ret = send_command("update \"#{path}\"")
1011
+ else
1012
+ ret = send_command('update')
1013
+ end
1014
+
1015
+ return(ret.gsub('updating_db: ', '').to_i)
1016
+ end
1017
+
1018
+ #
1019
+ # Private Method
1020
+ #
1021
+ # Used to send a command to the server. This synchronizes
1022
+ # on a mutex to be thread safe
1023
+ #
1024
+ # Returns the server response as processed by `handle_server_response`,
1025
+ # Raises a RuntimeError if the command failed
1026
+ def send_command( command )
1027
+ if @socket.nil?
1028
+ raise "MPD: Not Connected to the Server"
1029
+ end
1030
+
1031
+ ret = nil
1032
+
1033
+ @mutex.synchronize do
1034
+ begin
1035
+ @socket.puts command
1036
+ ret = handle_server_response
1037
+ rescue Errno::EPIPE
1038
+ @socket = nil
1039
+ raise 'MPD Error: Broken Pipe (Disconnected)'
1040
+ end
1041
+ end
1042
+
1043
+ return ret
1044
+ end
1045
+
1046
+ #
1047
+ # Private Method
1048
+ #
1049
+ # Handles the server's response (called inside send_command)
1050
+ #
1051
+ # This will repeatedly read the server's response from the socket
1052
+ # and will process the output. If a string is returned by the server
1053
+ # that is what is returned. If just an "OK" is returned, this returns
1054
+ # true. If an "ACK" is returned, this raises an error
1055
+ def handle_server_response
1056
+ return if @socket.nil?
1057
+
1058
+ msg = ''
1059
+ reading = true
1060
+ error = nil
1061
+ while reading
1062
+ line = @socket.gets
1063
+ case line
1064
+ when "OK\n"
1065
+ reading = false
1066
+ when /^ACK/
1067
+ error = line
1068
+ reading = false
1069
+ when nil
1070
+ reading = false
1071
+ else
1072
+ msg += line
1073
+ end
1074
+ end
1075
+
1076
+ if error.nil?
1077
+ return true if msg.empty?
1078
+ return msg
1079
+ else
1080
+ raise error.gsub( /^ACK \[(\d+)\@(\d+)\] \{(.+)\} (.+)$/, 'MPD Error #\1: \3: \4')
1081
+ end
1082
+ end
1083
+
1084
+ #
1085
+ # Private Method
1086
+ #
1087
+ # This builds a hash out of lines returned from the server.
1088
+ # First the response is turned into an array of lines
1089
+ # then each entry is parsed so that the line is viewed as
1090
+ # "key: value"
1091
+ #
1092
+ # The end result is a hash containing the proper key/value pairs
1093
+ def build_hash( string )
1094
+ return {} if string.nil? or !string.kind_of? String
1095
+
1096
+ hash = {}
1097
+ lines = string.split "\n"
1098
+ lines.each do |line|
1099
+ hash[ line.gsub(/:.*/, '').downcase ] = line.gsub(/\A[^:]*: /, '')
1100
+ end
1101
+
1102
+ return hash
1103
+ end
1104
+
1105
+ #
1106
+ # Private Method
1107
+ #
1108
+ # This is similar to build_hash, but instead of building a Hash,
1109
+ # a MPD::Song is built
1110
+ def build_song( string )
1111
+ return if string.nil? or !string.kind_of? String
1112
+
1113
+ song = Song.new
1114
+ lines = string.split "\n"
1115
+ lines.each do |line|
1116
+ song[ line.gsub(/:.*/, '').downcase ] = line.gsub(/\A[^:]*: /, '')
1117
+ end
1118
+
1119
+ return song
1120
+ end
1121
+
1122
+ #
1123
+ # Private Method
1124
+ #
1125
+ # This first creates an array of lines as returned from the server
1126
+ # Then each entry is processed and added to an MPD::Song
1127
+ # Whenever a new 'file:' entry is found, the current MPD::Song
1128
+ # is added to an array, and a new one is created
1129
+ #
1130
+ # The end result is an Array of MPD::Songs
1131
+ def build_songs_list( string )
1132
+ return [] if string.nil? or !string.kind_of? String
1133
+
1134
+ list = []
1135
+ song = Song.new
1136
+ lines = string.split "\n"
1137
+ lines.each do |line|
1138
+ key = line.gsub(/:.*/, '')
1139
+ line.gsub!(/\A[^:]*: /, '')
1140
+
1141
+ if key == 'file' && !song.file.nil?
1142
+ list << song
1143
+ song = Song.new
1144
+ end
1145
+
1146
+ song[key.downcase] = line
1147
+ end
1148
+
1149
+ list << song
1150
+
1151
+ return list
1152
+ end
1153
+
1154
+ #
1155
+ # Private Method
1156
+ #
1157
+ # This filters each line from the server to return
1158
+ # only those matching the regexp. The regexp is removed
1159
+ # from the line before it is added to an Array
1160
+ #
1161
+ # This is used in the `directories`, `files`, etc methods
1162
+ # to return only the directory/file names
1163
+ def filter_response( string, regexp )
1164
+ list = []
1165
+ lines = string.split "\n"
1166
+ lines.each do |line|
1167
+ if line =~ regexp
1168
+ list << line.gsub(regexp, '')
1169
+ end
1170
+ end
1171
+
1172
+ return list
1173
+ end
1174
+
1175
+ private :send_command
1176
+ private :handle_server_response
1177
+ private :build_hash
1178
+ private :build_song
1179
+ private :build_songs_list
1180
+ private :filter_response
1181
+
1182
+ end