A Remote-Controlled Jukebox
What if you had a jukebox on your main computer that played random or selected items from your music collection? What if you could search your music collection and add items to the jukebox queue from a laptop in another room of the house?
Ruby can help you realize this super-geek dreamthe software part, anyway. In this recipe, Ill show you how to write a jukebox server that can be programmed from any computer on the local network.
The jukebox will consist of a client and a server. The server broadcasts its location to a nearby Rinda server so clients on the local network can find it without knowing the address. The client will look up the server with Rinda and then communicate with it via DRb.
What features should the jukebox have? When there are no clients interfering with its business, the server will pick random songs from a predefined playlist and play them. It will call out to external Unix programs to play songs on the local computers audio system (if you have a way of broadcasting songs through streaming audio, say, an IceCast server, it could use that instead).
A client can query the jukebox, stop or restart it, or request that a particular song be played. The jukebox will keep requests in a queue. Once it plays all the requests, it will resume playing songs at random.
Since well be running subprocesses to access the sound card on the computer that runs the jukebox, the Jukebox object can be distributed to another machine. Instead, we need to proxy it with DRbUndumped.
The first thing we need to do is start a RingServer somewhere on our local network. Heres a reprint of the RingServer program from Recipe 16.14:
#!/usr/bin/ruby # rinda_server.rb require inda/ring # for RingServer require inda/tuplespace # for TupleSpace DRb.start_service # Create a TupleSpace to hold named services, and start running. Rinda::RingServer.new(Rinda::TupleSpace.new) DRb.thread.join
Heres the jukebox server file. First, well define the Jukebox server class, and set up its basic behavior: to play its queue and pick randomly when the queue is empty.
#!/usr/bin/ruby -w # jukebox_server.rb require drb require inda/ring require inda/tuplespace require hread require find DRb.start_service class Jukebox include DRbUndumped attr_reader :now_playing, :running def initialize(files) @files = files @songs = @files.keys @now_playing = nil @queue = [] end def play_queue Thread.new(self) do @running = true while @running if @queue.empty? play songs[rand(songs.size)] else play @queue.shift end end end end
Next, well write the methods that a client can use:
# Adds a song to the queue. Returns the new size of the queue. def <<(song) raise ArgumentError, No such song unless @files[song] @queue.push song return @queue.size end # Returns the current queue of songs. def queue return @queue.clone.freeze end # Returns the titles of songs that match the given regexp. def songs(regexp=/.*/) return @songs.grep(regexp).sort end # Turns the jukebox on or off. def running=(value) @running = value play_queue if @running end
Finally, heres the code that actually plays a song, by calling out to a preinstalled programeither mpg123 or ogg123, depending on the extension of the song file:
private # Play the given through this computers sound system, using a # previously installed music player. def play(song) @now_playing = song path = @files[song] player = path[-4..path.size] == .mp3 ? mpg123 : ogg123 command = %{#{player} "#{path}"} # The player and path both come from local data, so its safe to # untaint them. command.untaint system(command) end end
Now we can use the Jukebox class in a script. This one treats ARGV as a list of directories. We descend each one looking for music files, and feed the results into a Jukebox:
if ARGV.empty? puts "Usage: #{__FILE__} [directory full of MP3s and/or OGGs] …" exit else songs = {} Find.find(*ARGV) do |path| if path =~ /.(mp3|ogg)$/ name = File.split(path)[1][0..-5] songs[name] = path end end end jukebox = Jukebox.new(songs)
So far there hasn been much distributed code, and there won be much total. But we do need to register the Jukebox object with Rinda so that clients can find it:
# Set safe before we start accepting connections from outside. $SAFE = 1 puts "Registering…" # Register the Jukebox with the local RingServer, under its class name. ring_server = Rinda::RingFinger.primary ring_server.write([:name, :Jukebox, jukebox, "Remote-controlled jukebox"], Rinda::SimpleRenewer.new)
Start the jukebox running, and we e in business:
jukebox.play_queue DRb.thread.join
Now we can query and manipulate the jukebox from an irb session on another computer:
require inda/ring require inda/tuplespace DRb.start_service ring_server = Rinda::RingFinger.primary jukebox = ring_server.read([:name, :Jukebox, nil, nil])[2] jukebox.now_playing # => "Chickadee" jukebox.songs(/D/) # => ["ID 3", "Don Leave Me Here (Over There Would Be Fine)"] jukebox << ID 3 # => 1 jukebox << "Attack of the Good Ol Boys from Planet Honky-Tonk" # ArgumentError: No such song jukebox.queue # => ["ID 3"]
But itll be easier to use if we write a real client program. Again, theres almost no DRb programming in the client, which is as it should be. Once we have the remote Jukebox object, we can use it just like we would a local object.
First, we have some preliminary argument checking:
#!/usr/bin/ruby -w # jukebox_client.rb require inda/ring NO_ARG_COMMANDS = %w{start stop now-playing queue} ARG_COMMANDS = %w{grep append grep-and-append} COMMANDS = NO_ARG_COMMANDS + ARG_COMMANDS def usage puts "Usage: #{__FILE__} [#{COMMANDS.join(|)}] [ARG]" exit end usage if ARGV.size < 1 or ARGV.size > 2 command = ARGV[0] argument = nil usage unless COMMANDS.index(command) if ARG_COMMANDS.index(command) if ARGV.size == 1 puts "Command #{command} takes an argument." exit else argument = ARGV[1] end elsif ARGV.size == 2 puts "Command #{command} takes no argument." exit end
Next, the only distributed code in the client: the fetch of the Jukebox object from the Rinda server.
DRb.start_service ring_server = Rinda::RingFinger.primary jukebox = ring_server.read([:name, :Jukebox, nil, nil])[2]
Now that we have the Jukebox object (rather, a proxy to the real Jukebox object on the other computer), we can apply the users desired command to it:
case command when start then if jukebox.running puts Already running. else jukebox.running = true puts Started. end when stop then if jukebox.running jukebox.running = false puts Jukebox will stop after current song. else puts Already stopped. end when ow-playing then puts "Currently playing: #{jukebox.now_playing}" when queue then jukebox.queue.each { |song| puts song } when grep jukebox.songs(Regexp.compile(argument)).each { |song| puts song } when append then jukebox << argument jukebox.queue.each { |song| puts song } when grep-and-append then jukebox.songs(Regexp.compile(argument)).each { |song| jukebox << song } jukebox.queue.each { |song| puts song } end
Some obvious enhancements to this program:
- Combine the server with the ID3 parser from Recipe 6.17 to provide more reliable title information, as well as artist and other metadata.
- Make the ID3 metadata searchable, so that you can search for songs by a particular band.
- Make the @songs data structure capable of handling multiple distinct songs with the same name.
- Make the selection keep track of song history, so that it doesn choose to play the same song twice in the row.
- Have the jukebox send its selections to a program that streams audio over the network, rather than to programs that play the music locally. This way you can listen to the jukebox from any computer in your house. Without this step, you need to wire your whole house for sound, or have really loud speakers, or a really small house (like mine).
See Also
- Recipe 6.17, "Processing a Binary File"
- Recipe 16.14, "Automatically Discovering DRb Services with Rinda"
- Recipe 16.15, "Proxying Objects That Can Be Distributed"
Категории