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.
(defun delete-songs (playlist &rest names-and-values)
(delete-rows
:from (songs-table playlist)
:where (apply #'matching (songs-table playlist) names-and-values))
(setf (current-idx playlist) (or (position-of-current playlist) 0))
(update-current-if-necessary playlist))
(defun position-of-current (playlist)
(let* ((table (songs-table playlist))
(matcher (matching table :file (file (current-song playlist))))
(pos 0))
(do-rows (row table)
(when (funcall matcher row)
(return-from position-of-current pos))
(incf pos))))
You can also provide a function to completely clear the playlist, which uses delete-all-rows
and doesn’t have to worry about finding the current song since it has obviously been deleted. The call to update-current-if-necessary
will take care of setting current-song
to **NIL**
.
(defun clear-playlist (playlist)
(delete-all-rows (songs-table playlist))
(setf (current-idx playlist) 0)
(update-current-if-necessary playlist))
Sorting and shuffling the playlist are related in that the playlist is always either sorted or shuffled. The shuffle
slot says whether the playlist should be shuffled and if so how. If it’s set to :none
, then the playlist is ordered according to the value in the ordering
slot. When shuffle
is :song
, the playlist will be randomly permuted. And when it’s set to :album
, the list of albums is randomly permuted, but the songs within each album are listed in track order. Thus, the sort-playlist
function, which will be called by the Web interface code whenever the user selects a new ordering, needs to set ordering
to the desired ordering and set shuffle
to :none
before calling order-playlist
, which actually does the sort. As in delete-songs
, you need to use position-of-current
to reset current-idx
to the new location of the current song. However, this time you don’t need to call update-current-if-necessary
since you know the current song is still in the table.
(defun sort-playlist (playlist ordering)
(setf (ordering playlist) ordering)
(setf (shuffle playlist) :none)
(order-playlist playlist)
(setf (current-idx playlist) (position-of-current playlist)))
In order-playlist
, you can use the database function sort-rows
to actually perform the sort, passing a list of columns to sort by based on the value of ordering
.
(defun order-playlist (playlist)
(apply #'sort-rows (songs-table playlist)
(case (ordering playlist)
(:genre '(:genre :album :track))
(:artist '(:artist :album :track))
(:album '(:album :track))
(:song '(:song)))))
The function shuffle-playlist
, called by the Web interface code when the user selects a new shuffle mode, works in a similar fashion except it doesn’t need to change the value of ordering
. Thus, when shuffle-playlist
is called with a shuffle
of :none
, the playlist goes back to being sorted according to the most recent ordering. Shuffling by songs is simple—just call shuffle-table
on songs-table
. Shuffling by albums is a bit more involved but still not rocket science.
(defun shuffle-playlist (playlist shuffle)
(setf (shuffle playlist) shuffle)
(case shuffle
(:none (order-playlist playlist))
(:song (shuffle-by-song playlist))
(:album (shuffle-by-album playlist)))
(setf (current-idx playlist) (position-of-current playlist)))
(defun shuffle-by-song (playlist)
(shuffle-table (songs-table playlist)))
(defun shuffle-by-album (playlist)
(let ((new-table (make-playlist-table)))
(do-rows (album-row (shuffled-album-names playlist))
(do-rows (song (songs-for-album playlist (column-value album-row :album)))
(insert-row song new-table)))
(setf (songs-table playlist) new-table)))
(defun shuffled-album-names (playlist)
(shuffle-table
(select
:columns :album
:from (songs-table playlist)
:distinct t)))
(defun songs-for-album (playlist album)
(select
:from (songs-table playlist)
:where (matching (songs-table playlist) :album album)
:order-by :track))
The last manipulation you need to support is setting the playlist’s repeat mode. Most of the time you don’t need to take any extra action when setting repeat
--its value comes into play only in maybe-move-to-next-song
. However, you need to update the current-song
as a result of changing repeat
in one situation, namely, if current-idx
is at the end of a nonempty playlist and repeat
is being changed to :song
or :all
. In that case, you want to continue playing, either repeating the last song or starting at the beginning of the playlist. So, you should define an :after
method on the generic function (setf repeat)
.
(defmethod (setf repeat) :after (value (playlist playlist))
(if (and (at-end-p playlist) (not (empty-p playlist)))
(ecase value
(:song (setf (current-idx playlist) (1- (table-size (songs-table playlist)))))
(:none)
(:all (setf (current-idx playlist) 0)))
(update-current-if-necessary playlist)))
Now you have all the underlying bits you need. All that remains is the code that will provide a Web-based user interface for browsing the MP3 database and manipulating playlists. The interface will consist of three main functions defined with define-url-function
: one for browsing the song database, one for viewing and manipulating a single playlist, and one for listing all the available playlists.
But before you get to writing these three functions, you need to start with some helper functions and HTML macros that they’ll use.