Выбрать главу

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

Query Parameter Types

Since you'll be using define-url-function, you need to define a few methods on the string->type generic function from Chapter 28 that define-url-function uses to convert string query parameters into Lisp objects. In this application, you'll need methods to convert strings to integers, keyword symbols, and a list of values.

The first two are quite simple.

(defmethod string->type ((type (eql 'integer)) value)

(parse-integer (or value "") :junk-allowed t))

(defmethod string->type ((type (eql 'keyword)) value)

(and (plusp (length value)) (intern (string-upcase value) :keyword)))

The last string->type method is slightly more complex. For reasons I'll get to in a moment, you'll need to generate pages that display a form that contains a hidden field whose value is a list of strings. Since you're responsible for generating the value in the hidden field and for parsing it when it comes back, you can use whatever encoding is convenient. You could use the functions WRITE-TO-STRING and READ-FROM-STRING, which use the Lisp printer and reader to write and read data to and from strings, except the printed representation of strings can contain quotation marks and other characters that may cause problems when embedded in the value attribute of an INPUT element. So, you'll need to escape those characters somehow. Rather than trying to come up with your own escaping scheme, you can just use base 64, an encoding commonly used to protect binary data sent through e-mail. AllegroServe comes with two functions, base64-encode and base64-decode, that do the encoding and decoding for you, so all you have to do is write a pair of functions: one that encodes a Lisp object by converting it to a readable string with WRITE-TO-STRING and then base 64 encoding it and, conversely, another to decode such a string by base 64 decoding it and passing the result to READ-FROM-STRING. You'll want to wrap the calls to WRITE-TO-STRING and READ-FROM-STRING in WITH-STANDARD-IO-SYNTAX to make sure all the variables that affect the printer and reader are set to their standard values. However, because you're going to be reading data that's coming in from the network, you'll definitely want to turn off one feature of the reader—the ability to evaluate arbitrary Lisp code while reading![309] You can define your own macro with-safe-io-syntax, which wraps its body forms in WITH-STANDARD-IO-SYNTAX wrapped around a LET that binds *READ-EVAL* to NIL.

вернуться

309

The reader supports a bit of syntax, #., that causes the following s-expression to be evaluated at read time. This is occasionally useful in source code but obviously opens a big security hole when you read untrusted data. However, you can turn off this syntax by setting *READ-EVAL* to NIL, which will cause the reader to signal an error if it encounters #..