An mpd clone in Ruby
I was bored, so I decided to write a clone of my music player of choice, the excellent mpd. I also put together a little client, similar to my favorite mpd client, the highly usable mpc.
These are of course watered-down clones that cannot match the original mpd. I wasn’t so much interested in writing an alternative to mdp as just writing something for the fun of it. Feel free to improve upon it.
You will need to have Rubygame installed on the machine that is running the server. There are alternatives that you could look into, but I happen to know Rubygame so I am using it. It is likely that I will at some point experiment with other libraries.
Server/client communication is done via XML-RPC. This makes everything very simple. Again, if you want to practice, you change to some other method for client/server communication (SOAP, your own protocol, or whatever).
Other things you could do include writing a GUI client (I recommend Tk, but Qt4 is also nice, Gtk less so, but at least its Ruby bindings are decent to work with). Or, if you think everything should run in the browser, why not a web client? Of course, you can also add more features. In that case I recommend looking into what mpd supports and try to implement as much of it as possible.
First the server, rmpd:
1 #! /usr/bin/ruby
2
3 # A music playing daemon written in Ruby.
4
5 begin
6 require ‘rubygame‘
7 require ‘xmlrpc/server‘
8 require ‘optparse‘
9 rescue LoadError => e
10 msg, lib = e.to_s.split /\s+–\s+/
11 abort "Unable to find #{lib}, cannot continue."
12 end
13
14 class PlayerService
15 def initialize(player)
16 @player = player
17 end
18
19 def add(song)
20 @player.add song.to_i
21 end
22
23 def lsc
24 @player.lsc
25 end
26
27 def lsq
28 @player.lsq
29 end
30
31 def play
32 @player.play
33 end
34
35 def next
36 @player.next
37 end
38
39 def stop
40 @player.stop
41 end
42 end
43
44 class ControllerService
45 def initialize(player)
46 @player = player
47 end
48
49 def shutdown
50 @player.shutdown
51 end
52 end
53
54 class Player
55 def initialize(path, port=8080, addr=‘*‘)
56 @col = []
57 @queue = []
58 @current = nil
59 @control = nil
60
61 parsedir(path)
62
63 @server = XMLRPC::Server.new port, addr
64 @server.add_handler "player", PlayerService.new(self)
65 @server.add_handler "controller", ControllerService.new(self)
66 @server.set_default_handler do |name, *args|
67 obj, meth = name.split /\./
68 "No such command: #{meth}"
69 end
70
71 Rubygame.init
72 Rubygame::Mixer.open_audio
73 end
74
75 def run
76 @server.serve
77 end
78
79 def add(song)
80 @queue << @col[song]
81 "Added #{song}: #{File.basename(@col[song])}"
82 end
83
84 def lsc
85 return "Collection is empty." if @col.empty?
86
87 str = ""
88 @col.each_index do |idx|
89 str += "#{idx.to_s.rjust(3)}: #{File.basename(@col[idx])}\n"
90 end
91
92 return str
93 end
94
95 def lsq
96 return "Queue is empty." if @queue.empty?
97
98 str = ""
99 @queue.each_index do |idx|
100 str += "#{idx.to_s.rjust(3)}: #{File.basename(@queue[idx])}\n"
101 end
102
103 return str
104 end
105
106 def play
107 return "Already playing" if !@current.nil? && @current.playing?
108 return "Queue is empty" if @queue.empty?
109
110 begin
111 song = @queue.shift
112 @current = Rubygame::Mixer::Music.load_audio song
113 @current.play
114 control
115 "Playing #{File.basename(song)}"
116 rescue
117 "Something went wrong"
118 end
119 end
120
121 def next
122 return "Not playing." if @current.nil? || !@current.playing?
123 @control.exit unless @control.nil?
124 @current.stop
125 sleep 1 while @current.playing?
126 play
127 end
128
129 def stop
130 return "Not playing." if @current.nil? || !@current.playing?
131
132 @control.exit unless @control.nil?
133 @current.stop
134 "Stopped."
135 end
136
137 def control
138 @control = Thread.new {
139 sleep 1 while @current.playing?
140 play unless @queue.empty?
141 }
142
143 @control.run
144 end
145
146 def shutdown
147 @server.shutdown
148 Rubygame::Mixer.close_audio
149 Rubygame.quit
150 "Shutting down server."
151 end
152
153 def parsedir(path)
154 Dir.new(path).each do |file|
155 next if file =~ /^\./
156
157 if File.directory? "#{path}/#{file}"
158 parsedir "#{path}/#{file}"
159 else
160 @col << "#{path}/#{file}"
161 end
162 end
163 end
164 end
165
166 options = {
167 :addr => ‘127.0.0.1‘,
168 :port => 8080,
169 :dir => "#{ENV['HOME']}/music"
170 }
171
172 begin
173 OptionParser.new do |opts|
174 opts.banner = "usage: #{$0} [options]"
175
176 opts.on("-a", "–address=ADDR", String, "Address to listen on.") do |addr|
177 options[:addr] = addr
178 end
179
180 opts.on("-p", "–port=PORT", Integer, "Port to listen on.") do |port|
181 options[:port] = port
182 end
183
184 opts.on("-d", "–dir=DIR", String, "Collection directory.") do |dir|
185 options[:dir] = dir
186 end
187 end.parse!
188 rescue
189 abort "Command line foo!"
190 end
191
192 mpd = Player.new options[:dir], options[:port], options[:addr]
193 mpd.run
Here is the client, rmpc:
1 #! /usr/bin/ruby
2
3 # A client for rmpd.
4
5 require ‘xmlrpc/client‘
6 require ‘optparse‘
7
8 options = {
9 :port => 8080,
10 :addr => "127.0.0.1"
11 }
12
13 begin
14 OptionParser.new do |opts|
15 opts.banner = "usage: #{$0} [options] command"
16
17 opts.on("-a", "–address=ADDR", String, "Address of server") do |addr|
18 options[:addr] = addr
19 end
20
21 opts.on("-p", "–port=PORT", Integer, "Port of server") do |port|
22 options[:port] = port
23 end
24 end.parse!
25 rescue
26 abort "Command line foo!"
27 end
28
29 abort "No command." if ARGV.empty?
30
31 begin
32 server = XMLRPC::Client.new options[:addr], "/RPC2", options[:port]
33 rescue
34 abort "No contact with server."
35 end
36
37 player = server.proxy "player"
38 control = server.proxy "controller"
39
40 command = ARGV.shift
41 if command == ‘shutdown‘
42 puts control.shutdown
43 else
44 puts player.send(command, *ARGV)
45 end



Cool! I was doing the same stuff :p
But then based on jruby/gstreamer library
Comment by LeonB — 2008.04.07 @ 07:10