(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 silence[308] 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))))
Manipulating the Playlist
The rest of the playlist code is functions used by the Web interface to manipulate playlist
objects, including adding and deleting songs, sorting and shuffling, and setting the repeat mode. As in the helper functions in the previous section, you don't need to worry about locking in these functions because, as you'll see, the lock will be acquired in the Web interface function that calls these.
Adding and deleting is mostly a question of manipulating the songs-table
. The only extra work you have to do is to keep the current-song
and current-idx
in sync. For instance, whenever the playlist is empty, its current-idx
will be zero, and the current-song
will be the *empty-playlist-song*
. If you add a song to an empty playlist, then the index of zero is now in bounds, and you should change the current-song
to the newly added song. By the same token, when you've played all the songs in a playlist and current-song
is *end-of-playlist-song*
, adding a song should cause current-song
to be reset. All this really means, though, is that you need to call update-current-if-necessary
at the appropriate points.
Adding songs to a playlist is a bit involved because of the way the Web interface communicates which songs to add. For reasons I'll discuss in the next section, the Web interface code can't just give you a simple set of criteria to use in selecting songs from the database. Instead, it gives you the name of a column and a list of values, and you're supposed to add all the songs from the main database where the given column has a value in the list of values. Thus, to add the right songs, you need to first build a table object containing the desired values, which you can then use with an in
query against the song database. So, add-songs
looks like this:
(defun add-songs (playlist column-name values)
(let ((table (make-instance
'table
:schema (extract-schema (list column-name) (schema *mp3s*)))))
(dolist (v values) (insert-row (list column-name v) table))
(do-rows (row (select :from *mp3s* :where (in column-name table)))
(insert-row row (songs-table playlist))))
(update-current-if-necessary playlist))
Deleting songs is a bit simpler; you just need to be able to delete songs from the songs-table
that match particular criteria—either a particular song or all songs in a particular genre, by a particular artist, or from a particular album. So, you can provide a delete-songs
function that takes keyword/value pairs, which are used to construct a matching :where
clause you can pass to the delete-rows
database function.
Another complication that arises when deleting songs is that current-idx
may need to change. Assuming the current song isn't one of the ones just deleted, you'd like it to remain the current song. But if songs before it in songs-table
are deleted, it'll be in a different position in the table after the delete. So after a call to delete-rows
, you need to look for the row containing the current song and reset current-idx
. If the current song has itself been deleted, then, for lack of anything better to do, you can reset current-idx
to zero. After updating current-idx
, calling update-current-if-necessary
will take care of updating current-song
. And if current-idx
changed but still points at the same song, current-song
will be left alone.
308
Unfortunately, because of licensing issues around the MP3 format, it's not clear that it's legal for me to provide you with such an MP3 without paying licensing fees to Fraunhofer IIS. I got mine as part of the software that came with my Slimp3 from Slim Devices. You can grab it from their Subversion repository via the Web at http://svn.slimdevices.com/*checkout*/trunk/server/HTML/EN/html/silentpacket.mp3?rev=2
. Or buy a Squeezebox, the new, wireless version of Slimp3, and you'll get silentpacket.mp3
as part of the software that comes with it. Or find an MP3 of John Cage's piece