Keeping it Small and Simple

2008.04.05

An mpd clone in Ruby

Filed under: Ruby Programming, Rubygame — Tags: , , , , , , , — Lorenzo E. Danielsson @ 18:02

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

Advertisements

1 Comment »

  1. Cool! I was doing the same stuff :p
    But then based on jruby/gstreamer library

    Comment by LeonB — 2008.04.07 @ 07:10


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: