ruby-mpd 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/lib/mpdserver.rb ADDED
@@ -0,0 +1,1206 @@
1
+ #
2
+ #== mpdserver.rb
3
+ #
4
+ # This is the test server for librmpd. It is a 'shallow' server,
5
+ # it implements only the client/server protocol in a highly
6
+ # scriptable manner. This means you can set up your own simple
7
+ # test music database for testing an mpd client. You can now
8
+ # distribute your unit tests (you do have unit tests, yes?) along
9
+ # with a test database (a YAML file), and anyone can check that
10
+ # your client is in working order.
11
+ #
12
+ #== Usage
13
+ #
14
+ # The MPD Server is a subclass of GServer, so you have a lot of
15
+ # flexibility at your disposal. The constructor of the server object
16
+ # takes the port, an optional YAML database file, and any args for GServer.
17
+ #
18
+ # The YAML database file can be one of your own creation, or you can use
19
+ # one supplied by librmpd (default)
20
+ #
21
+ # Example:
22
+ #
23
+ # require 'rubygems'
24
+ # require 'librmpd'
25
+ # require 'mpdserver'
26
+ #
27
+ # server = MPDTestServer.new 7700
28
+ # server.start
29
+ #
30
+ # You can then enable auditing to see what commands are run by a client:
31
+ #
32
+ # server.audit = true
33
+ #
34
+ # This will print any commands from a client to stdout
35
+ #
36
+ #=== Unit Testing
37
+ #
38
+ # For unit testing a client using the test server, I recommend using the
39
+ # set up and tear down methods to initialize and destroy a test server.
40
+ #
41
+ # def setup
42
+ # @server = MPDTestServer.new 7700
43
+ # @server.start
44
+ # end
45
+ #
46
+ # def teardown
47
+ # @server.stop
48
+ # end
49
+ #
50
+ # This will ensure you are using a clean server instance for each test.
51
+
52
+ require 'gserver'
53
+ require 'yaml'
54
+
55
+ class MPDTestServer < GServer
56
+
57
+ def initialize( port, db_file = nil, *args )
58
+ super port, *args
59
+
60
+ if db_file.nil?
61
+ db_file = __FILE__.gsub(/\/[^\/]*$/, '') + '/../data/database.yaml'
62
+ end
63
+
64
+ @status = {
65
+ :volume => 0,
66
+ :repeat => 0,
67
+ :random => 0,
68
+ :playlist => 1,
69
+ :state => 'stop',
70
+ :xfade => 0
71
+ }
72
+ @elapsed_time = 0
73
+ @current_song = nil
74
+ @database = YAML::load( File.open( db_file ) )
75
+ @songs = @database[0]
76
+ @playlists = @database[1]
77
+ @artists = []
78
+ @albums = []
79
+ @titles = []
80
+ @the_playlist = []
81
+ @playback_thread = nil
82
+ @filetree = {:name =>'', :dirs =>[], :songs =>[]}
83
+ @songs.each_with_index do |song,i|
84
+ song['id'] = i
85
+ if !song['artist'].nil? and !@artists.include? song['artist']
86
+ @artists << song['artist']
87
+ end
88
+ if !song['album'].nil? and !@albums.include? song['album']
89
+ @albums << song['album']
90
+ end
91
+ if !song['title'].nil?
92
+ @titles << song['title']
93
+ end
94
+ if !song['file'].nil?
95
+ dirs = song['file'].split '/'
96
+ dirs.pop
97
+ the_dir = @filetree
98
+ dirs.each do |d|
99
+ found = nil
100
+ the_dir[:dirs].each do |sub|
101
+ if sub[:name] == d
102
+ found = sub
103
+ break
104
+ end
105
+ end
106
+ if found.nil?
107
+ found = {:name => d, :dirs =>[], :songs =>[]}
108
+ the_dir[:dirs] << found
109
+ end
110
+ the_dir = found
111
+ end # End dirs.each
112
+ the_dir[:songs] << song
113
+ end # End if !song['file'].nil?
114
+ end # End @songs.each
115
+
116
+ sort_dir @filetree
117
+ @artists.sort!
118
+ @albums.sort!
119
+ @titles.sort!
120
+ end
121
+
122
+ def start
123
+ super
124
+
125
+ @playback_thread = Thread.new(@status, self) do |status, server|
126
+ while not server.stopped?
127
+ if status[:state] == 'play'
128
+ song = server.get_current_song
129
+ if song.nil?
130
+ server.elapsed_time = 0
131
+ status[:state] = 'stop'
132
+ next
133
+ end
134
+
135
+ status[:time] = "#{server.elapsed_time}:#{song['time']}"
136
+ status[:bitrate] = 192
137
+ status[:audio] = '44100:16:2'
138
+
139
+ if server.elapsed_time >= song['time'].to_i
140
+ server.elapsed_time = 0
141
+ server.next_song
142
+ end
143
+
144
+ server.elapsed_time = server.elapsed_time + 1
145
+ elsif status[:state] == 'pause'
146
+ song = server.get_current_song
147
+ if song.nil?
148
+ server.elapsed_time = 0
149
+ status[:state] = 'stop'
150
+ next
151
+ end
152
+ status[:time] = "#{server.elapsed_time}:#{song['time']}"
153
+ status[:bitrate] = 192
154
+ status[:audio] = '44100:16:2'
155
+ else
156
+ status[:time] = nil
157
+ status[:bitrate] = nil
158
+ status[:audio] = nil
159
+ server.elapsed_time = 0
160
+ end
161
+ sleep 1
162
+ end
163
+ end
164
+ end
165
+
166
+ def serve( sock )
167
+ command_list = []
168
+ in_cmd_list = false
169
+ in_ok_list = false
170
+ the_error = nil
171
+ sock.puts 'OK MPD 0.11.5'
172
+ begin
173
+ while line = sock.gets
174
+
175
+ args = build_args line
176
+
177
+ cmd = args.shift
178
+
179
+ if cmd == 'command_list_begin' and args.length == 0 and !in_cmd_list
180
+ in_cmd_list = true
181
+ log 'MPD: Starting Command List' if audit
182
+ elsif cmd == 'command_list_ok_begin' and args.length == 0 and !in_cmd_list
183
+ in_cmd_list = true
184
+ in_ok_list = true
185
+ log 'MPD: Starting Command OK List' if audit
186
+ elsif cmd == 'command_list_end' and in_cmd_list
187
+ log 'MPD: Running Command List' if audit
188
+
189
+ the_ret = true
190
+ command_list.each_with_index do |set,i|
191
+ the_ret = do_cmd sock, set[0], set[1]
192
+
193
+ if audit
194
+ log "MPD Command List: CMD ##{i}: \"#{set[0]}(#{set[1].join(', ')})\": " + (the_ret ? 'successful' : 'failed')
195
+ end
196
+
197
+ break unless the_ret
198
+
199
+ sock.puts 'list_OK' if in_ok_list
200
+
201
+ end
202
+
203
+ sock.puts 'OK' if the_ret
204
+
205
+ command_list.clear
206
+ in_cmd_list = false
207
+ in_ok_list = false
208
+ else
209
+ if in_cmd_list
210
+ command_list << [cmd, args]
211
+ else
212
+ ret = do_cmd sock, cmd, args
213
+ sock.puts 'OK' if ret
214
+ if audit
215
+ log "MPD Command \"#{cmd}(#{args.join(', ')})\": " + (ret ? 'successful' : 'failed')
216
+ end # End if audit
217
+ end # End if in_cmd_list
218
+ end # End if cmd == 'comand_list_begin' ...
219
+ end # End while line = sock.gets
220
+ rescue
221
+ end
222
+ end
223
+
224
+ def do_cmd( sock, cmd, args )
225
+ case cmd
226
+ when 'add'
227
+ if args.length == 0
228
+ # Add the entire database
229
+ @songs.each do |s|
230
+ s['_mod_ver'] = @status[:playlist]
231
+ incr_version
232
+ @the_playlist << s
233
+ end
234
+ return true
235
+ else
236
+ # Add a single entry
237
+ the_song = nil
238
+ @songs.each do |s|
239
+ if s['file'] == args[0]
240
+ the_song = s
241
+ break
242
+ end
243
+ end
244
+
245
+ if the_song.nil?
246
+ dir = locate_dir(args[0])
247
+ if not dir.nil?
248
+ # Add the dir
249
+ add_dir_to_pls dir
250
+ return true
251
+ else
252
+ return(cmd_fail(sock,'ACK [50@0] {add} directory or file not found'))
253
+ end
254
+ else
255
+ the_song['_mod_ver'] = @status[:playlist]
256
+ incr_version
257
+ @the_playlist << the_song
258
+ return true
259
+ end
260
+ end
261
+ when 'clear'
262
+ args_check( sock, cmd, args, 0 ) do
263
+ incr_version
264
+ @the_playlist = []
265
+ @current_song = nil
266
+ return true
267
+ end
268
+ when 'clearerror'
269
+ args_check( sock, cmd, args, 0 ) do
270
+ the_error = nil
271
+ return true
272
+ end
273
+ when 'close'
274
+ sock.close
275
+ return true
276
+ when 'crossfade'
277
+ args_check( sock, cmd, args, 1 ) do |args|
278
+ if is_int(args[0]) and args[0].to_i >= 0
279
+ @status[:xfade] = args[0].to_i
280
+ return true
281
+ else
282
+ return(cmd_fail(sock,"ACK [2@0] {crossfade} \"#{args[0]}\" is not a integer >= 0"))
283
+ end
284
+ end
285
+ when 'currentsong'
286
+ args_check( sock, cmd, args, 0 ) do
287
+ if @current_song != nil and @current_song < @the_playlist.length
288
+ send_song sock, @the_playlist[@current_song]
289
+ end
290
+ return true
291
+ end
292
+ when 'delete'
293
+ args_check( sock, cmd, args, 1 ) do |args|
294
+ if is_int args[0]
295
+ if args[0].to_i < 0 or args[0].to_i >= @the_playlist.length
296
+ return(cmd_fail(sock,"ACK [50@0] {delete} song doesn't exist: \"#{args[0]}\""))
297
+ else
298
+ @the_playlist.delete_at args[0].to_i
299
+ args[0].to_i.upto @the_playlist.length - 1 do |i|
300
+ @the_playlist[i]['_mod_ver'] = @status[:playlist]
301
+ end
302
+ incr_version
303
+ return true
304
+ end
305
+ else
306
+ return(cmd_fail('ACK [2@0] {delete} need a positive integer'))
307
+ end
308
+ end
309
+ when 'deleteid'
310
+ args_check( sock, cmd, args, 1 ) do |args|
311
+ if is_int args[0]
312
+ the_song = nil
313
+ @the_playlist.each do |song|
314
+ if song['id'] == args[0].to_i
315
+ the_song = song
316
+ break
317
+ end
318
+ end
319
+
320
+ if not the_song.nil?
321
+ index = @the_playlist.index the_song
322
+ @the_playlist.delete the_song
323
+ index.upto @the_playlist.length - 1 do |i|
324
+ @the_playlist[i]['_mod_ver'] = @status[:playlist]
325
+ end
326
+ incr_version
327
+ return true
328
+ else
329
+ return(cmd_fail(sock,"ACK [50@0] {deleteid} song id doesn't exist: \"#{args[0]}\""))
330
+ end
331
+ else
332
+ return(cmd_fail(sock,'ACK [2@0] {deleteid} need a positive integer'))
333
+ end
334
+ end
335
+ when 'find'
336
+ args_check( sock, cmd, args, 2 ) do |args|
337
+ if args[0] != 'album' and args[0] != 'artist' and args[0] != 'title'
338
+ return(cmd_fail(sock,'ACK [2@0] {find} incorrect arguments'))
339
+ else
340
+ if args[0] == 'album'
341
+ @songs.each do |song|
342
+ if song['album'] == args[1]
343
+ send_song sock, song
344
+ end
345
+ end
346
+ elsif args[0] == 'artist'
347
+ @songs.each do |song|
348
+ if song['artist'] == args[1]
349
+ send_song sock, song
350
+ end
351
+ end
352
+ elsif args[0] == 'title'
353
+ @songs.each do |song|
354
+ if song['title'] == args[1]
355
+ send_song sock, song
356
+ end
357
+ end
358
+ end
359
+ return true
360
+ end
361
+ end
362
+ when 'kill'
363
+ args_check( sock, cmd, args, 0 ) do
364
+ sock.close
365
+ return true
366
+ end
367
+ when 'list'
368
+ args_check( sock, cmd, args, 1..2 ) do |args|
369
+ if args[0] != 'album' and args[0] != 'artist' and args[0] != 'title'
370
+ return(cmd_fail(sock,"ACK [2@0] {list} \"#{args[0]}\" is not known"))
371
+ elsif args[0] == 'artist' and args.length > 1
372
+ return(cmd_fail(sock,'ACK [2@0] {list} should be "Album" for 3 arguments'))
373
+ else
374
+ if args[0] == 'artist'
375
+ # List all Artists
376
+ @artists.each do |artist|
377
+ sock.puts "Artist: #{artist}"
378
+ end
379
+ return true
380
+ elsif args[0] == 'title'
381
+ # List all Titles
382
+ @titles.each do |title|
383
+ sock.puts "Title: #{title}"
384
+ end
385
+ return true
386
+ else
387
+ if args.length == 2
388
+ # List all Albums by Artist
389
+ # artist == args[1]
390
+ listed = []
391
+ @songs.each do |song|
392
+ if song['artist'] == args[1]
393
+ if not song['album'].nil? and !listed.include? song['album']
394
+ sock.puts "Album: #{song['album']}"
395
+ listed << song['album']
396
+ end
397
+ end
398
+ end
399
+ return true
400
+ else
401
+ # List all Albums
402
+ @albums.each do |album|
403
+ sock.puts "Album: #{album}"
404
+ end
405
+ return true
406
+ end
407
+ end
408
+ end
409
+ end
410
+ when 'listall'
411
+ args_check( sock, cmd, args, 0..1 ) do |args|
412
+ if args.length == 0
413
+ @filetree[:dirs].each do |d|
414
+ send_dir sock, d, false
415
+ end
416
+ else
417
+ was_song = false
418
+ @songs.each do |song|
419
+ if song['file'] == args[0]
420
+ sock.puts "file: #{song['file']}"
421
+ was_song = true
422
+ break
423
+ end
424
+ end
425
+
426
+ if was_song
427
+ return true
428
+ end
429
+
430
+ dir = locate_dir args[0]
431
+ if not dir.nil?
432
+ parents = args[0].split '/'
433
+ parents.pop
434
+ parents = parents.join '/'
435
+ parents += '/' unless parents.length == 0
436
+ send_dir sock, dir, false, parents
437
+ else
438
+ return(cmd_fail(sock,'ACK [50@0] {listall} directory or file not found'))
439
+ end
440
+ end
441
+ return true
442
+ end
443
+ when 'listallinfo'
444
+ args_check( sock, cmd, args, 0..1 ) do |args|
445
+ if args.length == 0
446
+ @filetree[:dirs].each do |d|
447
+ send_dir sock, d, true
448
+ end
449
+ else
450
+ was_song = false
451
+ @songs.each do |song|
452
+ if song['file'] == args[0]
453
+ send_song song
454
+ was_song = true
455
+ break
456
+ end
457
+ end
458
+
459
+ if was_song
460
+ return true
461
+ end
462
+
463
+ dir = locate_dir args[0]
464
+ if not dir.nil?
465
+ parents = args[0].split '/'
466
+ parents.pop
467
+ parents = parents.join '/'
468
+ parents += '/' unless parents.length == 0
469
+ send_dir sock, dir, true, parents
470
+ else
471
+ return(cmd_fail(sock,'ACK [50@0] {listallinfo} directory or file not found'))
472
+ end
473
+ end
474
+ return true
475
+ end
476
+ when 'load'
477
+ args_check( sock, cmd, args, 1 ) do
478
+ # incr_version for each song loaded
479
+ pls = args[0] + '.m3u'
480
+ the_pls = nil
481
+ @playlists.each do |p|
482
+ if p['file'] == pls
483
+ the_pls = p
484
+ break
485
+ end
486
+ end
487
+
488
+ unless the_pls.nil?
489
+ the_pls['songs'].each do |song|
490
+ song['_mod_ver'] = @status[:playlist]
491
+ @the_playlist << song
492
+ incr_version
493
+ end
494
+ else
495
+ return(cmd_fail(sock,"ACK [50@0] {load} playlist \"#{args[0]}\" not found"))
496
+ end
497
+ end
498
+ when 'lsinfo'
499
+ args_check( sock, cmd, args, 0..1 ) do
500
+ if args.length == 0
501
+ @filetree[:dirs].each do |d|
502
+ sock.puts "directory: #{d[:name]}"
503
+ d[:songs].each do |s|
504
+ send_song sock, s
505
+ end
506
+ end
507
+ @playlists.each do |pls|
508
+ sock.puts "playlist: #{pls['file'].gsub( /\.m3u$/, '' )}"
509
+ end
510
+ else
511
+ dir = locate_dir args[0]
512
+ if dir.nil?
513
+ return(cmd_fail(sock,"ACK [50@0] {lsinfo} directory not found"))
514
+ else
515
+ dir[:dirs].each do |d|
516
+ sock.puts "directory: #{args[0] + '/' + d[:name]}"
517
+ end
518
+ dir[:songs].each do |s|
519
+ send_song sock, s
520
+ end
521
+ end
522
+ end
523
+ return true
524
+ end
525
+ when 'move'
526
+ args_check( sock, cmd, args, 2 ) do |args|
527
+ if !is_int args[0]
528
+ return(cmd_fail(sock,"ACK [2@0] {move} \"#{args[0]}\" is not a integer"))
529
+ elsif !is_int args[1]
530
+ return(cmd_fail(sock,"ACK [2@0] {move} \"#{args[1]}\" is not a integer"))
531
+ elsif args[0].to_i < 0 or args[0].to_i >= @the_playlist.length
532
+ return(cmd_fail(sock,"ACK [50@0] {move} song doesn't exist: \"#{args[0]}\""))
533
+ elsif args[1].to_i < 0 or args[1].to_i >= @the_playlist.length
534
+ return(cmd_fail(sock,"ACK [50@0] {move} song doesn't exist: \"#{args[1]}\""))
535
+ else
536
+ tmp = @the_playlist.delete_at args[0].to_i
537
+ @the_playlist.insert args[1].to_i, tmp
538
+ if args[0].to_i < args[1].to_i
539
+ args[0].to_i.upto args[1].to_i do |i|
540
+ @the_playlist[i]['_mod_ver'] = @status[:playlist]
541
+ end
542
+ else
543
+ args[1].to_i.upto args[0].to_i do |i|
544
+ @the_playlist[i]['_mod_ver'] = @status[:playlist]
545
+ end
546
+ end
547
+ incr_version
548
+ return true
549
+ end
550
+ end
551
+ when 'moveid'
552
+ args_check( sock, cmd, args, 2 ) do |args|
553
+ if !is_int args[0]
554
+ return(cmd_fail(sock,"ACK [2@0] {moveid} \"#{args[0]}\" is not a integer"))
555
+ elsif !is_int args[1]
556
+ return(cmd_fail(sock,"ACK [2@0] {moveid} \"#{args[1]}\" is not a integer"))
557
+ elsif args[1].to_i < 0 or args[1].to_i >= @the_playlist.length
558
+ return(cmd_fail(sock,"ACK [50@0] {moveid} song doesn't exist: \"#{args[1]}\""))
559
+ else
560
+ # Note: negative args should be checked
561
+ the_song = nil
562
+ index = -1
563
+ @the_playlist.each_with_index do |song,i|
564
+ if song['id'] == args[0].to_i
565
+ the_song = song
566
+ index = i
567
+ end
568
+ end
569
+ if the_song.nil?
570
+ return(cmd_fail(sock,"ACK [50@0] {moveid} song id doesn't exist: \"#{args[0]}\""))
571
+ end
572
+ tmp = @the_playlist.delete_at index
573
+ @the_playlist.insert args[1].to_i, tmp
574
+ if index < args[1].to_i
575
+ index.upto args[1].to_i do |i|
576
+ @the_playlist[i]['_mod_ver'] = @status[:playlist]
577
+ end
578
+ else
579
+ args[1].to_i.upto index do |i|
580
+ @the_playlist[i]['_mod_ver'] = @status[:playlist]
581
+ end
582
+ end
583
+ incr_version
584
+ return true
585
+ end
586
+ end
587
+ when 'next'
588
+ args_check( sock, cmd, args, 0 ) do
589
+ if @status[:state] != 'stop'
590
+ next_song
591
+ @elapsed_time = 0
592
+ @status[:state] = 'play'
593
+ end
594
+ return true
595
+ end
596
+ when 'pause'
597
+ args_check( sock, cmd, args, 0..1 ) do |args|
598
+ if args.length > 0 and not is_bool args[0]
599
+ return(cmd_fail(sock,"ACK [2@0] {pause} \"#{args[0]}\" is not 0 or 1"))
600
+ end
601
+
602
+ if @status[:state] != 'stop'
603
+ if args.length == 1
604
+ @status[:state] = ( args[0] == '1' ? 'pause' : 'play' )
605
+ else
606
+ @status[:state] = ( @status[:state] == 'pause' ? 'play' : 'pause' )
607
+ end
608
+ end
609
+
610
+ return true
611
+ end
612
+ when 'password'
613
+ args_check( sock, cmd, args, 1 ) do |args|
614
+ return true if args[0] == 'test'
615
+ return(cmd_fail(sock,"ACK [3@0] {password} incorrect password"))
616
+ end
617
+ when 'ping'
618
+ args_check( sock, cmd, args, 0 ) do
619
+ return true
620
+ end
621
+ when 'play'
622
+ args_check( sock, cmd, args, 0..1 ) do |args|
623
+ if args.length > 0 and !is_int(args[0])
624
+ return(cmd_fail(sock,'ACK [2@0] {play} need a positive integer'))
625
+ else
626
+ args.clear if args[0] == '-1'
627
+ if args.length == 0
628
+ if @the_playlist.length > 0 and @status[:state] != 'play'
629
+ @current_song = 0 if @current_song.nil?
630
+ @elapsed_time = 0
631
+ @status[:state] = 'play'
632
+ end
633
+ else
634
+ if args[0].to_i < 0 or args[0].to_i >= @the_playlist.length
635
+ return(cmd_fail(sock,"ACK [50@0] {play} song doesn't exist: \"#{args[0]}\""))
636
+ end
637
+
638
+ @current_song = args[0].to_i
639
+ @elapsed_time = 0
640
+ @status[:state] = 'play'
641
+ end
642
+ return true
643
+ end
644
+ end
645
+ when 'playid'
646
+ args_check( sock, cmd, args, 0..1 ) do |args|
647
+ if args.length > 0 and !is_int(args[0])
648
+ return(cmd_fail(sock,'ACK [2@0] {playid} need a positive integer'))
649
+ else
650
+ args.clear if args[0] == '-1'
651
+ if args.length == 0
652
+ if @the_playlist.length > 0 and @status[:state] != 'play'
653
+ @current_song = 0 if @current_song.nil?
654
+ @elapsed_time = 0
655
+ @status[:state] = 'play'
656
+ end
657
+ else
658
+ index = nil
659
+ @the_playlist.each_with_index do |s,i|
660
+ if s['id'] == args[0].to_i
661
+ index = i
662
+ break;
663
+ end
664
+ end
665
+
666
+ return(cmd_fail(sock,"ACK [50@0] {playid} song id doesn't exist: \"#{args[0]}\"")) if index.nil?
667
+
668
+ @current_song = index
669
+ @elapsed_time = 0
670
+ @status[:state] = 'play'
671
+ end
672
+ return true
673
+ end
674
+ end
675
+ when 'playlist'
676
+ log 'MPD Warning: Call to Deprecated API: "playlist"' if audit
677
+ args_check( sock, cmd, args, 0 ) do
678
+ @the_playlist.each_with_index do |v,i|
679
+ sock.puts "#{i}:#{v['file']}"
680
+ end
681
+ return true
682
+ end
683
+ when 'playlistinfo'
684
+ args_check( sock, cmd, args, 0..1 ) do |args|
685
+ if args.length > 0 and !is_int(args[0])
686
+ return(cmd_fail(sock,'ACK [2@0] {playlistinfo} need a positive integer'))
687
+ else
688
+ args.clear if args.length > 0 and args[0].to_i < 0
689
+ if args.length != 0
690
+ if args[0].to_i >= @the_playlist.length
691
+ return(cmd_fail(sock,"ACK [50@0] {playlistinfo} song doesn't exist: \"#{args[0]}\""))
692
+ else
693
+ song = @the_playlist[args[0].to_i]
694
+ send_song sock, song
695
+ sock.puts "Pos: #{args[0].to_i}"
696
+ sock.puts "Id: #{song['id']}"
697
+ return true
698
+ end
699
+ else
700
+ @the_playlist.each_with_index do |song,i|
701
+ send_song sock, song
702
+ sock.puts "Pos: #{i}"
703
+ sock.puts "Id: #{song['id']}"
704
+ end
705
+ return true
706
+ end
707
+ end
708
+ end
709
+ when 'playlistid'
710
+ args_check( sock, cmd, args, 0..1 ) do |args|
711
+ if args.length > 0 and !is_int(args[0])
712
+ return(cmd_fail(sock,'ACK [2@0] {playlistid} need a positive integer'))
713
+ else
714
+ song = nil
715
+ pos = nil
716
+ args.clear if args[0].to_i < 0
717
+ if args.length != 0
718
+ @the_playlist.each_with_index do |s,i|
719
+ if s['id'] == args[0].to_i
720
+ song = s
721
+ pos = i
722
+ break;
723
+ end
724
+ end
725
+
726
+ return(cmd_fail(sock,"ACK [50@0] {playlistid} song id doesn't exist: \"#{args[0]}\"")) if song.nil?
727
+
728
+ send_song sock, song
729
+ sock.puts "Pos: #{pos}"
730
+ return true
731
+ else
732
+ @the_playlist.each_with_index do |song,i|
733
+ send_song sock, song
734
+ sock.puts "Pos: #{i}"
735
+ end
736
+ return true
737
+ end
738
+ end
739
+ end
740
+ when 'plchanges'
741
+ args_check( sock, cmd, args, 1 ) do |args|
742
+ if args.length > 0 and !is_int(args[0])
743
+ return(cmd_fail(sock,'ACK [2@0] {plchanges} need a positive integer'))
744
+ else
745
+ # Note: args[0] < 0 just return OK...
746
+ @the_playlist.each_with_index do |song,i|
747
+ if args[0].to_i > @status[:playlist] or song['_mod_ver'] >= args[0].to_i or song['_mod_ver'] == 0
748
+ send_song sock, song
749
+ sock.puts "Pos: #{i}"
750
+ end
751
+ end
752
+ return true
753
+ end
754
+ end
755
+ when 'plchangesposid'
756
+ args_check( sock, cmd, args, 1 ) do |args|
757
+ if args.length > 0 and !is_int(args[0])
758
+ return(cmd_fail(sock,'ACK [2@0] {plchangesposid} need a positive integer'))
759
+ else
760
+ # Note: args[0] < 0 just return OK...
761
+ @the_playlist.each_with_index do |song,i|
762
+ if args[0].to_i > @status[:playlist] or song['_mod_ver'] >= args[0].to_i or song['_mod_ver'] == 0
763
+ sock.puts "cpos: #{i}"
764
+ sock.puts "Id: #{song['id']}"
765
+ end
766
+ end
767
+ return true
768
+ end
769
+ end
770
+ when 'previous'
771
+ args_check( sock, cmd, args, 0 ) do
772
+ return true if @status[:state] == 'stop'
773
+ prev_song
774
+ @elapsed_time = 0
775
+ @status[:state] = 'play'
776
+ return true
777
+ end
778
+ when 'random'
779
+ args_check( sock, cmd, args, 1 ) do |args|
780
+ if is_bool args[0]
781
+ @status[:random] = args[0].to_i
782
+ return true
783
+ elsif is_int args[0]
784
+ return(cmd_fail(sock,"ACK [2@0] {random} \"#{args[0]}\" is not 0 or 1"))
785
+ else
786
+ return(cmd_fail(sock,'ACK [2@0] {random} need an integer'))
787
+ end
788
+ end
789
+ when 'repeat'
790
+ args_check( sock, cmd, args, 1 ) do |args|
791
+ if is_bool args[0]
792
+ @status[:repeat] = args[0].to_i
793
+ return true
794
+ elsif is_int args[0]
795
+ return(cmd_fail(sock,"ACK [2@0] {repeat} \"#{args[0]}\" is not 0 or 1"))
796
+ else
797
+ return(cmd_fail(sock,'ACK [2@0] {repeat} need an integer'))
798
+ end
799
+ end
800
+ when 'rm'
801
+ args_check( sock, cmd, args, 1 ) do |args|
802
+ rm_pls = args[0] + '.m3u'
803
+ the_pls = -1
804
+ @playlists.each_with_index do |pls,i|
805
+ the_pls = i if pls['file'] == rm_pls
806
+ end
807
+
808
+ if the_pls != -1
809
+ @playlists.delete_at the_pls
810
+ return true
811
+ else
812
+ return(cmd_fail(sock,"ACK [50@0] {rm} playlist \"#{args[0]}\" not found"))
813
+ end
814
+ end
815
+ when 'save'
816
+ args_check( sock, cmd, args, 1 ) do |args|
817
+ new_playlist = {'file' => args[0]+'.m3u', 'songs' => @the_playlist}
818
+ @playlists << new_playlist
819
+ return true
820
+ end
821
+ when 'search'
822
+ args_check( sock, cmd, args, 2 ) do |args|
823
+ if args[0] != 'title' and args[0] != 'artist' and args[0] != 'album' and args[0] != 'filename'
824
+ return(cmd_fail(sock,'ACK [2@0] {search} incorrect arguments'))
825
+ end
826
+ args[0] = 'file' if args[0] == 'filename'
827
+ @songs.each do |song|
828
+ data = song[args[0]]
829
+ if not data.nil? and data.downcase.include? args[1]
830
+ send_song sock, song
831
+ end
832
+ end
833
+ return true
834
+ end
835
+ when 'seek'
836
+ args_check( sock, cmd, args, 2 ) do |args|
837
+ if !is_int args[0]
838
+ return(cmd_fail(sock,"ACK [2@0] {seek} \"#{args[0]}\" is not a integer"))
839
+ elsif !is_int args[1]
840
+ return(cmd_fail(sock,"ACK [2@0] {seek} \"#{args[1]}\" is not a integer"))
841
+ else
842
+ if args[0].to_i > @the_playlist.length or args[0].to_i < 0
843
+ return(cmd_fail(sock,"ACK [50@0] {seek} song doesn't exist: \"#{args[0]}\""))
844
+ end
845
+ args[1] = '0' if args[1].to_i < 0
846
+ song = @the_playlist[args[0].to_i]
847
+ if args[1].to_i >= song['time'].to_i
848
+ if args[0].to_i + 1 < @the_playlist.length
849
+ @current_song = args[0].to_i + 1
850
+ @elapsed_time = 0
851
+ @status[:state] = 'play' unless @status[:state] == 'pause'
852
+ else
853
+ @current_song = nil
854
+ @elapsed_time = 0
855
+ @status[:state] = 'stop'
856
+ end
857
+ else
858
+ @current_song = args[0].to_i
859
+ @elapsed_time = args[1].to_i
860
+ @status[:state] = 'play' unless @status[:state] == 'pause'
861
+ end
862
+ return true
863
+ end
864
+ end
865
+ when 'seekid'
866
+ args_check( sock, cmd, args, 2 ) do |args|
867
+ if !is_int args[0]
868
+ return(cmd_fail(sock,"ACK [2@0] {seekid} \"#{args[0]}\" is not a integer"))
869
+ elsif !is_int args[1]
870
+ return(cmd_fail(sock,"ACK [2@0] {seekid} \"#{args[1]}\" is not a integer"))
871
+ else
872
+ pos = nil
873
+ song = nil
874
+ @the_playlist.each_with_index do |s,i|
875
+ if s['id'] == args[0].to_i
876
+ song = s
877
+ pos = i
878
+ break;
879
+ end
880
+ end
881
+
882
+ if song.nil?
883
+ return(cmd_fail(sock,"ACK [50@0] {seekid} song id doesn't exist: \"#{args[0]}\""))
884
+ end
885
+
886
+ args[1] = '0' if args[1].to_i < 0
887
+ if args[1].to_i >= song['time'].to_i
888
+ if pos + 1 < @the_playlist.length
889
+ @current_song = pos + 1
890
+ @elapsed_time = 0
891
+ @status[:state] = 'play' unless @status[:state] == 'pause'
892
+ else
893
+ @current_song = nil
894
+ @elapsed_time = 0
895
+ @status[:state] = 'stop'
896
+ end
897
+ else
898
+ @current_song = pos
899
+ @elapsed_time = args[1].to_i
900
+ @status[:state] = 'play' unless @status[:state] == 'pause'
901
+ end
902
+ return true
903
+ end
904
+ end
905
+ when 'setvol'
906
+ args_check( sock, cmd, args, 1 ) do |args|
907
+ if !is_int args[0]
908
+ return(cmd_fail(sock,'ACK [2@0] {setvol} need an integer'))
909
+ else
910
+ # Note: args[0] < 0 actually sets the vol val to < 0
911
+ @status[:volume] = args[0].to_i
912
+ return true
913
+ end
914
+ end
915
+ when 'shuffle'
916
+ args_check( sock, cmd, args, 0 ) do
917
+ @the_playlist.each do |s|
918
+ s['_mod_ver'] = @status[:playlist]
919
+ end
920
+ incr_version
921
+ @the_playlist.reverse!
922
+ return true
923
+ end
924
+ when 'stats'
925
+ args_check( sock, cmd, args, 0 ) do
926
+ # artists
927
+ sock.puts "artists: #{@artists.size}"
928
+ # albums
929
+ sock.puts "albums: #{@albums.size}"
930
+ # songs
931
+ sock.puts "songs: #{@songs.size}"
932
+ # uptime
933
+ sock.puts "uptime: 500"
934
+ # db_playtime
935
+ time = 0
936
+ @songs.each do |s|
937
+ time += s['time'].to_i
938
+ end
939
+ sock.puts "db_playtime: #{time}"
940
+ # db_update
941
+ sock.puts "db_update: 1159418502"
942
+ # playtime
943
+ sock.puts "playtime: 10"
944
+ return true
945
+ end
946
+ when 'status'
947
+ args_check( sock, cmd, args, 0 ) do
948
+ @status.each_pair do |key,val|
949
+ sock.puts "#{key}: #{val}" unless val.nil?
950
+ end
951
+ sock.puts "playlistlength: #{@the_playlist.length}"
952
+
953
+ if @current_song != nil and @the_playlist.length > @current_song
954
+ sock.puts "song: #{@current_song}"
955
+ sock.puts "songid: #{@the_playlist[@current_song]['id']}"
956
+ end
957
+
958
+ @status[:updating_db] = nil
959
+ return true
960
+ end
961
+ when 'stop'
962
+ args_check( sock, cmd, args, 0 ) do
963
+ @status[:state] = 'stop'
964
+ @status[:time] = nil
965
+ @status[:bitrate] = nil
966
+ @status[:audio] = nil
967
+ return true
968
+ end
969
+ when 'swap'
970
+ args_check( sock, cmd, args, 2 ) do |args|
971
+ if !is_int args[0]
972
+ return(cmd_fail(sock,"ACK [2@0] {swap} \"#{args[0]}\" is not a integer"))
973
+ elsif !is_int args[1]
974
+ return(cmd_fail(sock,"ACK [2@0] {swap} \"#{args[1]}\" is not a integer"))
975
+ elsif args[0].to_i >= @the_playlist.length or args[0].to_i < 0
976
+ return(cmd_fail(sock,"ACK [50@0] {swap} song doesn't exist: \"#{args[0]}\""))
977
+ elsif args[1].to_i >= @the_playlist.length or args[1].to_i < 0
978
+ return(cmd_fail(sock,"ACK [50@0] {swap} song doesn't exist: \"#{args[1]}\""))
979
+ else
980
+ tmp = @the_playlist[args[1].to_i]
981
+ @the_playlist[args[1].to_i] = @the_playlist[args[0].to_i]
982
+ @the_playlist[args[0].to_i] = tmp
983
+ @the_playlist[args[0].to_i]['_mod_ver'] = @status[:playlist]
984
+ @the_playlist[args[1].to_i]['_mod_ver'] = @status[:playlist]
985
+ incr_version
986
+ return true
987
+ end
988
+ end
989
+ when 'swapid'
990
+ args_check( sock, cmd, args, 2 ) do |args|
991
+ if !is_int args[0]
992
+ return(cmd_fail(sock,"ACK [2@0] {swapid} \"#{args[0]}\" is not a integer"))
993
+ elsif !is_int args[1]
994
+ return(cmd_fail(sock,"ACK [2@0] {swapid} \"#{args[1]}\" is not a integer"))
995
+ else
996
+ from = nil
997
+ to = nil
998
+ @the_playlist.each_with_index do |song,i|
999
+ if song['id'] == args[0].to_i
1000
+ from = i
1001
+ elsif song['id'] == args[1].to_i
1002
+ to = i
1003
+ end
1004
+ end
1005
+ if from.nil?
1006
+ return(cmd_fail(sock,"ACK [50@0] {swapid} song id doesn't exist: \"#{args[0]}\""))
1007
+ elsif to.nil?
1008
+ return(cmd_fail(sock,"ACK [50@0] {swapid} song id doesn't exist: \"#{args[1]}\""))
1009
+ end
1010
+ tmp = @the_playlist[to]
1011
+ @the_playlist[to] = @the_playlist[from]
1012
+ @the_playlist[from] = tmp
1013
+ @the_playlist[to]['_mod_ver'] = @status[:playlist]
1014
+ @the_playlist[from]['_mod_ver'] = @status[:playlist]
1015
+
1016
+ incr_version
1017
+ return true
1018
+ end
1019
+ end
1020
+ when 'update'
1021
+ args_check( sock, cmd, args, 0..1 ) do |args|
1022
+ incr_version
1023
+ sock.puts 'updating_db: 1'
1024
+ @status[:updating_db] = '1'
1025
+ return true
1026
+ end
1027
+ when 'volume'
1028
+ log 'MPD Warning: Call to Deprecated API: "volume"' if audit
1029
+ args_check( sock, cmd, args, 1 ) do |args|
1030
+ if !is_int args[0]
1031
+ return(cmd_fail(sock,'ACK [2@0] {volume} need an integer'))
1032
+ else
1033
+ # Note: args[0] < 0 subtract from the volume
1034
+ @status[:volume] += args[0].to_i
1035
+ return true
1036
+ end
1037
+ end
1038
+ else
1039
+ return(cmd_fail(sock,"ACK [5@0] {} unknown command #{cmd}"))
1040
+ end # End Case cmd
1041
+ end
1042
+
1043
+ def get_current_song
1044
+ if @current_song != nil and @current_song < @the_playlist.length
1045
+ return @the_playlist[@current_song]
1046
+ else
1047
+ return nil
1048
+ end
1049
+ end
1050
+
1051
+ def prev_song
1052
+ return if @current_song.nil?
1053
+ if @current_song == 0
1054
+ @elapsed_time = 0
1055
+ else
1056
+ @current_song -= 1
1057
+ end
1058
+ end
1059
+
1060
+ def next_song
1061
+ return if @current_song.nil?
1062
+ @current_song = (@current_song +1 < @the_playlist.length ? @current_song +1 : nil)
1063
+ end
1064
+
1065
+ def elapsed_time=( new_time )
1066
+ @elapsed_time = new_time
1067
+ end
1068
+
1069
+ def elapsed_time
1070
+ @elapsed_time
1071
+ end
1072
+
1073
+ def incr_version
1074
+ if @status[:playlist] == 2147483647
1075
+ @status[:playlist] = 1
1076
+ @the_playlist.each do |song|
1077
+ song['_mod_ver'] = 0
1078
+ end
1079
+ else
1080
+ @status[:playlist] += 1
1081
+ end
1082
+ end
1083
+
1084
+ def cmd_fail( sock, msg )
1085
+ sock.puts msg
1086
+ return false
1087
+ end
1088
+
1089
+ def build_args( line )
1090
+ ret = []
1091
+ word = ''
1092
+ escaped = false
1093
+ in_quote = false
1094
+
1095
+ line.strip!
1096
+
1097
+ line.each_byte do |c|
1098
+ c = c.chr
1099
+ if c == ' ' and !in_quote
1100
+ ret << word unless word.empty?
1101
+ word = ''
1102
+ elsif c == '"' and !escaped
1103
+ if in_quote
1104
+ in_quote = false
1105
+ else
1106
+ in_quote = true
1107
+ end
1108
+ ret << word unless word.empty?
1109
+ word = ''
1110
+ else
1111
+ escaped = (c == '\\')
1112
+ word += c
1113
+ end
1114
+ end
1115
+
1116
+ ret << word unless word.empty?
1117
+
1118
+ return ret
1119
+ end
1120
+
1121
+ def args_check( sock, cmd, argv, argc )
1122
+ if (argc.kind_of? Range and argc.include?(argv.length)) or
1123
+ (argv.length == argc)
1124
+ yield argv
1125
+ else
1126
+ sock.puts "ACK [2@0] {#{cmd}} wrong number of arguments for \"#{cmd}\""
1127
+ end
1128
+ end
1129
+
1130
+ def is_int( val )
1131
+ val =~ /^[-+]?[0-9]*$/
1132
+ end
1133
+
1134
+ def is_bool( val )
1135
+ val == '0' or val == '1'
1136
+ end
1137
+
1138
+ def locate_dir( path )
1139
+ dirs = path.split '/'
1140
+
1141
+ the_dir = @filetree
1142
+ dirs.each do |d|
1143
+ found = nil
1144
+ the_dir[:dirs].each do |sub|
1145
+ if sub[:name] == d
1146
+ found = sub
1147
+ break
1148
+ end
1149
+ end
1150
+ if found.nil?
1151
+ return nil
1152
+ else
1153
+ the_dir = found
1154
+ end
1155
+ end
1156
+
1157
+ return the_dir
1158
+ end
1159
+
1160
+ def send_song( sock, song )
1161
+ return if song.nil?
1162
+ sock.puts "file: #{song['file']}"
1163
+ song.each_pair do |key,val|
1164
+ sock.puts "#{key.capitalize}: #{val}" unless key == 'file' or key == '_mod_ver'
1165
+ end
1166
+ end
1167
+
1168
+ def send_dir( sock, dir, allinfo, path = '' )
1169
+ sock.puts "directory: #{path}#{dir[:name]}"
1170
+
1171
+ dir[:songs].each do |song|
1172
+ if allinfo
1173
+ send_song sock, song
1174
+ else
1175
+ sock.puts "file: #{song['file']}"
1176
+ end
1177
+ end
1178
+
1179
+ dir[:dirs].each do |d|
1180
+ send_dir(sock, d, allinfo, dir[:name] + '/')
1181
+ end
1182
+ end
1183
+
1184
+ def add_dir_to_pls( dir )
1185
+ dir[:songs].each do |song|
1186
+ song['_mod_ver'] = @status[:playlist]
1187
+ incr_version
1188
+ @the_playlist << song
1189
+ end
1190
+
1191
+ dir[:dirs].each do |d|
1192
+ add_dir_to_pls d
1193
+ end
1194
+ end
1195
+
1196
+ def sort_dir( dir )
1197
+ dir[:dirs].sort! do |x,y|
1198
+ x[:name] <=> y[:name]
1199
+ end
1200
+
1201
+ dir[:dirs].each do |d|
1202
+ sort_dir d
1203
+ end
1204
+ end
1205
+
1206
+ end