The Playlist
This brings me to the next URL function, playlist
. This is the most complex page of the three—it's responsible for displaying the current contents of the user's playlist as well as for providing the interface to manipulate the playlist. But with most of the tedious bookkeeping handled by define-url-function
, it's not too hard to see how playlist
works. Here's the beginning of the definition, with just the parameter list:
(define-url-function playlist
(request
(playlist-id string (playlist-id request) :package)
(action keyword) ; Playlist manipulation action
(what keyword :file) ; for :add-songs action
(values base-64-list) ; "
file ; for :add-songs and :delete-songs actions
genre ; for :delete-songs action
artist ; "
album ; "
(order-by keyword) ; for :sort action
(shuffle keyword) ; for :shuffle action
(repeat keyword)) ; for :set-repeat action
In addition to the obligatory request
parameter, playlist
takes a number of query parameters. The most important in some ways is playlist-id
, which identifies which playlist
object the page should display and manipulate. For this parameter, you can take advantage of define-url-function
's "sticky parameter" feature. Normally, the playlist-id
won't be supplied explicitly, defaulting to the value returned by the playlist-id
function, namely, the IP address of the client machine on which the browser is running. However, users can also manipulate their playlists from different machines than the ones running their MP3 clients by allowing this value to be explicitly specified. And if it's specified once, define-url-function
will arrange for it to "stick" by setting a cookie in the browser. Later you'll define a URL function that generates a list of all existing playlists, which users can use to pick a playlist other than the one for the machines they're browsing from.
The action
parameter specifies some action to take on the user's playlist object. The value of this parameter, which will be converted to a keyword symbol for you, can be :add-songs
, :delete-songs
, :clear
, :sort
, :shuffle
, or :set-repeat
. The :add-songs
action is used by the "Add all" button in the browse page and also by the links used to add individual songs. The other actions are used by the links on the playlist page itself.
The file
, what
, and values
parameters are used with the :add-songs
action. By declaring values
to be of type base-64-list
, the define-url-function
infrastructure will take care of decoding the value submitted by the "Add all" form. The other parameters are used with other actions as noted in the comments.
Now let's look at the body of playlist
. The first thing you need to do is use the playlist-id
to look up the queue object and then acquire the playlist's lock with the following two lines:
(let ((playlist (lookup-playlist playlist-id)))
(with-playlist-locked (playlist)
Since lookup-playlist
will create a new playlist if necessary, this will always return a playlist
object. Then you take care of any necessary queue manipulation, dispatching on the value of the action
parameter in order to call one of the playlist
functions.
(case action
(:add-songs (add-songs playlist what (or values (list file))))
(:delete-songs (delete-songs
playlist
:file file :genre genre
:artist artist :album album))
(:clear (clear-playlist playlist))
(:sort (sort-playlist playlist order-by))
(:shuffle (shuffle-playlist playlist shuffle))
(:set-repeat (setf (repeat playlist) repeat)))
All that's left of the playlist
function is the actual HTML generation. Again, you can use the :mp3-browser-page
HTML macro to make sure the basic form of the page matches the other pages in the application, though this time you pass NIL
to the :header
argument in order to leave out the H1
header. Here's the rest of the function:
(html
(:mp3-browser-page
(:title (:format "Playlist - ~a" (id playlist)) :header nil)
(playlist-toolbar playlist)
(if (empty-p playlist)
(html (:p (:i "Empty.")))
(html
((:table :class "playlist")
(:table-row "#" "Song" "Album" "Artist" "Genre")
(let ((idx 0)
(current-idx (current-idx playlist)))
(do-rows (row (songs-table playlist))
(with-column-values (track file song album artist genre) row
(let ((row-style (if (= idx current-idx) "now-playing" "normal")))
(html
((:table-row :class row-style)
track
(:progn song (delete-songs-link :file file))
(:progn album (delete-songs-link :album album))
(:progn artist (delete-songs-link :artist artist))
(:progn genre (delete-songs-link :genre genre)))))
(incf idx))))))))))))
The function playlist-toolbar
generates a toolbar containing links to playlist
to perform the various :action
manipulations. And delete-songs-link
generates a link to playlist
with the :action
parameter set to :delete-songs
and the appropriate arguments to delete an individual file, or all files on an album, by a particular artist or in a specific genre.
(defun playlist-toolbar (playlist)
(let ((current-repeat (repeat playlist))
(current-sort (ordering playlist))
(current-shuffle (shuffle playlist)))
(html
(:p :class "playlist-toolbar"
(:i "Sort by:")
" [ "
(sort-playlist-button "genre" current-sort) " | "
(sort-playlist-button "artist" current-sort) " | "
(sort-playlist-button "album" current-sort) " | "
(sort-playlist-button "song" current-sort) " ] "
(:i "Shuffle by:")
" [ "
(playlist-shuffle-button "none" current-shuffle) " | "
(playlist-shuffle-button "song" current-shuffle) " | "
(playlist-shuffle-button "album" current-shuffle) " ] "