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 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