librmpd 0.1.0

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/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