A Remote-Controlled Jukebox

Table of contents:

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"


Strings

Numbers

Date and Time

Arrays

Hashes

Files and Directories

Code Blocks and Iteration

Objects and Classes8

Modules and Namespaces

Reflection and Metaprogramming

XML and HTML

Graphics and Other File Formats

Databases and Persistence

Internet Services

Web Development Ruby on Rails

Web Services and Distributed Programming

Testing, Debugging, Optimizing, and Documenting

Packaging and Distributing Software

Automating Tasks with Rake

Multitasking and Multithreading

User Interface

Extending Ruby with Other Languages

System Administration



Ruby Cookbook
Ruby Cookbook (Cookbooks (OReilly))
ISBN: 0596523696
EAN: 2147483647
Year: N/A
Pages: 399

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net