Playlists As Song Sources
To use playlist
s as a source of songs for the Shoutcast server, you’ll need to implement a method on the generic function find-song-source
from Chapter 28. Since you’re going to have multiple playlists, you need a way to find the right one for each client that connects to the server. The mapping part is easy—you can define a variable that holds an **EQUAL**
hash table that you can use to map from some identifier to the playlist
object.
(defvar *playlists* (make-hash-table :test #'equal))
You’ll also need to define a process lock to protect access to this hash table like this:
(defparameter *playlists-lock* (make-process-lock :name "playlists-lock"))
Then define a function that looks up a playlist given an ID, creating a new playlist
object if necessary and using with-process-lock
to ensure that only one thread at a time manipulates the hash table.1
(defun lookup-playlist (id)
(with-process-lock (*playlists-lock*)
(or (gethash id *playlists*)
(setf (gethash id *playlists*) (make-instance 'playlist :id id)))))
Then you can implement find-song-source
on top of that function and another, playlist-id
, that takes an AllegroServe request object and returns the appropriate playlist identifier. The find-song-source
function is also where you grab the User-Agent string out of the request object and stash it in the playlist object.
(defmethod find-song-source ((type (eql 'playlist)) request)
(let ((playlist (lookup-playlist (playlist-id request))))
(with-playlist-locked (playlist)
(let ((user-agent (header-slot-value request :user-agent)))
(when user-agent (setf (user-agent playlist) user-agent))))
playlist))
The trick, then, is how you implement playlist-id
, the function that extracts the identifier from the request object. You have a couple options, each with different implications for the user interface. You can pull whatever information you want out of the request object, but however you decide to identify the client, you need some way for the user of the Web interface to get hooked up to the right playlist.
For now you can take an approach that “just works” as long as there’s only one MP3 client per machine connecting to the server and as long as the user is browsing the Web interface from the machine running the MP3 client: you’ll use the IP address of the client machine as the identifier. This way you can find the right playlist for a request regardless of whether the request is from the MP3 client or a Web browser. You will, however, provide a way in the Web interface to select a different playlist from the browser, so the only real constraint this choice puts on the application is that there can be only one connected MP3 client per client IP address.2 The implementation of playlist-id
looks like this:
(defun playlist-id (request)
(ipaddr-to-dotted (remote-host (request-socket request))))
The function request-socket
is part of AllegroServe, while remote-host
and ipaddr-to-dotted
are part of Allegro’s socket library.
To make a playlist usable as a song source by the Shoutcast server, you need to define methods on current-song
, still-current-p
, and maybe-move-to-next-song
that specialize their source
parameter on playlist
. The current-song
method is already taken care of: by defining the accessor current-song
on the eponymous slot, you automatically got a current-song
method specialized on playlist
that returns the value of that slot. However, to make accesses to the playlist
thread safe, you need to lock the playlist
before accessing the current-song
slot. In this case, the easiest way is to define an :around
method like the following:
(defmethod current-song :around ((playlist playlist))
(with-playlist-locked (playlist) (call-next-method)))
Implementing still-current-p
is also quite simple, assuming you can be sure that current-song
gets updated with a new song
object only when the current song actually changes. Again, you need to acquire the process lock to ensure you get a consistent view of the playlist
‘s state.
(defmethod still-current-p (song (playlist playlist))
(with-playlist-locked (playlist)
(eql song (current-song playlist))))
The trick, then, is to make sure the current-song
slot gets updated at the right times. However, the current song can change in a number of ways. The obvious one is when the Shoutcast server calls maybe-move-to-next-song
. But it can also change when songs are added to the playlist, when the Shoutcast server has run out of songs, or even if the playlist’s repeat mode is changed.
Rather than trying to write code specific to every situation to determine whether to update current-song
, you can define a function, update-current-if-necessary
, that updates current-song
if the song
object in current-song
no longer matches the file that the current-idx
slot says should be playing. Then, if you call this function after any manipulation of the playlist that could possibly put those two slots out of sync, you’re sure to keep current-song
set properly. Here are update-current-if-necessary
and its helper functions:
(defun update-current-if-necessary (playlist)
(unless (equal (file (current-song playlist))
(file-for-current-idx playlist))
(reset-current-song playlist)))
(defun file-for-current-idx (playlist)
(if (at-end-p playlist)
nil
(column-value (nth-row (current-idx playlist) (songs-table playlist)) :file)))
(defun at-end-p (playlist)
(>= (current-idx playlist) (table-size (songs-table playlist))))
You don’t need to add locking to these functions since they’ll be called only from functions that will take care of locking the playlist first.
The function reset-current-song
introduces one more wrinkle: because you want the playlist to provide an endless stream of MP3s to the client, you don’t want to ever set current-song
to **NIL**
. Instead, when a playlist runs out of songs to play—when songs-table
is empty or after the last song has been played and repeat
is set to :none
--then you need to set current-song
to a special song whose file is an MP3 of silence3 and whose title explains why no music is playing. Here’s some code to define two parameters, *empty-playlist-song*
and *end-of-playlist-song*
, each set to a song with the file named by *silence-mp3*
as their file and an appropriate title:
(defparameter *silence-mp3* ...)
(defun make-silent-song (title &optional (file *silence-mp3*))
(make-instance
'song
:file file
:title title
:id3-size (if (id3-p file) (size (read-id3 file)) 0)))
(defparameter *empty-playlist-song* (make-silent-song "Playlist empty."))
(defparameter *end-of-playlist-song* (make-silent-song "At end of playlist."))
reset-current-song
uses these parameters when the current-idx
doesn’t point to a row in songs-table
. Otherwise, it sets current-song
to a song
object representing the current row.
(defun reset-current-song (playlist)
(setf
(current-song playlist)
(cond
((empty-p playlist) *empty-playlist-song*)
((at-end-p playlist) *end-of-playlist-song*)
(t (row->song (nth-row (current-idx playlist) (songs-table playlist)))))))
(defun row->song (song-db-entry)
(with-column-values (file song artist album id3-size) song-db-entry
(make-instance
'song
:file file
:title (format nil "~a by ~a from ~a" song artist album)
:id3-size id3-size)))
(defun empty-p (playlist)
(zerop (table-size (songs-table playlist))))
Now, at last, you can implement the method on maybe-move-to-next-song
that moves current-idx
to its next value, based on the playlist’s repeat mode, and then calls update-current-if-necessary
. You don’t change current-idx
when it’s already at the end of the playlist because you want it to keep its current value, so it’ll point at the next song you add to the playlist. This function must lock the playlist before manipulating it since it’s called by the Shoutcast server code, which doesn’t do any locking.
(defmethod maybe-move-to-next-song (song (playlist playlist))
(with-playlist-locked (playlist)
(when (still-current-p song playlist)
(unless (at-end-p playlist)
(ecase (repeat playlist)
(:song) ; nothing changes
(:none (incf (current-idx playlist)))
(:all (setf (current-idx playlist)
(mod (1+ (current-idx playlist))
(table-size (songs-table playlist)))))))
(update-current-if-necessary playlist))))