Книга: Practical Common Lisp

Manipulating the 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
: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)
: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)
:columns :album
:from (songs-table playlist)
:distinct t)))
(defun songs-for-album (playlist album)
: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)))))
(: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.

Оглавление книги

Генерация: 1.072. Запросов К БД/Cache: 3 / 1
Вверх Вниз