Playlists

The basic idea behind the interface will be that each MP3 client that connects to the Shoutcast server gets its own playlist, which serves as the source of songs for the Shoutcast server. A playlist will also provide facilities beyond those needed by the Shoutcast server: through the Web interface the user will be able to add songs to the playlist, delete songs already in the playlist, and reorder the playlist by sorting and shuffling.

You can define a class to represent playlists like this:

  1. (defclass playlist ()
  2. ((id :accessor id :initarg :id)
  3. (songs-table :accessor songs-table :initform (make-playlist-table))
  4. (current-song :accessor current-song :initform *empty-playlist-song*)
  5. (current-idx :accessor current-idx :initform 0)
  6. (ordering :accessor ordering :initform :album)
  7. (shuffle :accessor shuffle :initform :none)
  8. (repeat :accessor repeat :initform :none)
  9. (user-agent :accessor user-agent :initform "Unknown")
  10. (lock :reader lock :initform (make-process-lock))))

The id of a playlist is the key you extract from the request object passed to find-song-source when looking up a playlist. You don’t actually need to store it in the playlist object, but it makes debugging a bit easier if you can find out from an arbitrary playlist object what its id is.

The heart of the playlist is the songs-table slot, which will hold a table object. The schema for this table will be the same as for the main MP3 database. The function make-playlist-table, which you use to initialize songs-table, is simply this:

  1. (defun make-playlist-table ()
  2. (make-instance 'table :schema *mp3-schema*))

The Package

You can define the package for the code in this chapter with the following **DEFPACKAGE**:

  1. (defpackage :com.gigamonkeys.mp3-browser
  2. (:use :common-lisp
  3. :net.aserve
  4. :com.gigamonkeys.html
  5. :com.gigamonkeys.shoutcast
  6. :com.gigamonkeys.url-function
  7. :com.gigamonkeys.mp3-database
  8. :com.gigamonkeys.id3v2)
  9. (:import-from :acl-socket
  10. :ipaddr-to-dotted
  11. :remote-host)
  12. (:import-from :multiprocessing
  13. :make-process-lock
  14. :with-process-lock)
  15. (:export :start-mp3-browser))

Because this is a high-level application, it uses a lot of lower-level packages. It also imports three symbols from the ACL-SOCKET package and two more from MULTIPROCESSING since it just needs those five and not the other 139 symbols those two packages export.

By storing the list of songs as a table, you can use the database functions from Chapter 27 to manipulate the playlist: you can add to the playlist with insert-row, delete songs with delete-rows, and reorder the playlist with sort-rows and shuffle-table.

The current-song and current-idx slots keep track of which song is playing: current-song is an actual song object, while current-idx is the index into the songs-table of the row representing the current song. You’ll see in the section “Manipulating the Playlist” how to make sure current-song is updated whenever current-idx changes.

The ordering and shuffle slots hold information about how the songs in songs-table are to be ordered. The ordering slot holds a keyword that tells how the songs-table should be sorted when it’s not shuffled. The legal values are :genre, :artist, :album, and :song. The shuffle slot holds one of the keywords :none, :song, or :album, which specifies how songs-table should be shuffled, if at all.

The repeat slot also holds a keyword, one of :none, :song, or :all, which specifies the repeat mode for the playlist. If repeat is :none, after the last song in the songs-table has been played, the current-song goes back to a default MP3. When :repeat is :song, the playlist keeps returning the same current-song forever. And if it’s :all, after the last song, current-song goes back to the first song.

The user-agent slot holds the value of the User-Agent header sent by the MP3 client in its request for the stream. You need to hold onto this value purely for use in the Web interface—the User-Agent header identifies the program that made the request, so you can display the value on the page that lists all the playlists to make it easier to tell which playlist goes with which connection when multiple clients connect.

Finally, the lock slot holds a process lock created with the function make-process-lock, which is part of Allegro’s MULTIPROCESSING package. You’ll need to use that lock in certain functions that manipulate playlist objects to ensure that only one thread at a time manipulates a given playlist object. You can define the following macro, built upon the with-process-lock macro from MULTIPROCESSING, to give an easy way to wrap a body of code that should be performed while holding a playlist’s lock:

  1. (defmacro with-playlist-locked ((playlist) &body body)
  2. `(with-process-lock ((lock ,playlist))
  3. ,@body))

The with-process-lock macro acquires exclusive access to the process lock given and then executes the body forms, releasing the lock afterward. By default, with-process-lock allows recursive locks, meaning the same thread can safely acquire the same lock multiple times.