index rss mastodon twitter github linkedin email
Álvaro Ramírez
sponsor

Álvaro Ramírez

16 August 2024 The dired abstraction

I recently wrote about image-mode's next/previous item navigation, a feature I wanted to bring to ready player mode.

I was curious to see how image-mode resolved next and previous files, so I checked the associated keybinding (n) via helpful-key (my preferred alternative to describe-key), and landed on image-next-file. While this function only takes care of high-level routing, it led me to image-mode--next-file, which is where the actual next/previous file resolution happens:

(defun image-mode--next-file (file n)
  "Go to the next image file in the parent buffer of FILE.
This is typically a Dired buffer, but may also be a tar/archive buffer.
Return the next image file from that buffer.
If N is negative, go to the previous file."
  ...)

While image-mode--next-file's implementation details are worth checking out, its docstring already highlights the bit I found most interesting: dired's involvement in the mix. I'm not sure why I initially found dired usage surprising. Buffers are Emacs's backbone. They are the fundamental structures holding the content we work with, whether it’s editing text, reading logs, displaying information, and many others including file management… Dired specializes buffers for this last purpose. While dired itself is a powerhouse, at its core it's just an ordered list of files.

Given a location within a dired buffer, we can use its helpers to find next and previous files. Like image-mode, ready-player now mirrors this approach (minus tar/archive handling). This got me thinking more about the dired abstraction… If it quacks like a duck, and walks like a duck, then it's probably errrm a dired buffer. What I actually mean is that associating a dired buffer to a ready-player buffer effectively attaches a playlist of sorts. It doesn't quite matter how this dired buffer was constructed. What's important is that it's recognized as a dired buffer, so all relevant helpers remain useful.

With dired buffers acting as media playlists, we can easily create a directory playlist by merely pointing dired to the current directory. This is the default behaviour in ready-player. When you open a media file, we attach a dired buffer pointing to the current directory. Play next or previous item, and you're effectively moving up and down the associated dired buffer.

Things get more interesting when we craft dired buffers in more creative ways than just supplying a path to a directory. One of my favourite commands is find-dired. It runs the find utility, crafting a dired buffer with its results.

find.png

For kicks, I added a ready-player-load-dired-playback-buffer command to ready-player, so we can just load any dired buffer, including our newly generated one, courtesy of find-dired.

With this generated buffer loaded and ready-player random playback enabled, we get to see our lucky jumps across find results.

find-random.gif

At this point I thought "this is prolly as far as I'll take things"… ready-player was born to address quick access to media, typically from dired itself. For deep playlist handling, there are many other Emacs media players.

The thing is, with my newly found reusable dired abstraction, a rough m3u playlist experiment didn't seem that far-fetched at all. I'd need to read an m3u file and generate a dired buffer. I knew nothing about m3u's, other than being text files including media paths, along with optional metadata. I figured minimal m3u reading support shouldn't be too difficult.

If we are to create a playlist including the first three album tracks from the artist above, it'd look something like this:

#EXTM3U

#EXTINF:-1,George Benson - Dance
/absolute/path/to/Music/George Benson/Body Talk/01 Dance.mp3
#EXTINF:-1,George Benson - When Love Has Grown
/absolute/path/to/Music/George Benson/Body Talk/02 When Love Has Grown.mp3
#EXTINF:-1,George Benson - Plum
/absolute/path/to/Music/George Benson/Body Talk/03 Plum.mp3

#EXTINF:-1,George Benson - So What
/absolute/path/to/Music/George Benson/Original Album Classics/1-01 So What.mp3
#EXTINF:-1,George Benson - The Gentle Rain
/absolute/path/to/Music/George Benson/Original Album Classics/1-02 The Gentle Rain (From the Film, _The Gentle Rain_).mp3
#EXTINF:-1,George Benson - All Clear
/absolute/path/to/Music/George Benson/Original Album Classics/1-03 All Clear.mp3

#EXTINF:-1,George Benson - Footin' It
/absolute/path/to/Music/George Benson/The Shape Of Things To Come/01 Footin' It.mp3
#EXTINF:-1,George Benson - Face It Boy It's Over
/absolute/path/to/Music/George Benson/The Shape Of Things To Come/02 Face It Boy It's Over.mp3
#EXTINF:-1,George Benson - Shape Of Things To Come
/absolute/path/to/Music/George Benson/The Shape Of Things To Come/03 Shape Of Things To Come.mp3

A crude function to extract file paths into a list would look something like the following:

(defun ready-player--media-at-m3u-file (m3u-path)
  "Read m3u playlist at M3U-PATH and return files."
  (with-temp-buffer
    (insert-file-contents m3u-path)
    (let ((files))
      (while (re-search-forward
              (rx bol (not (any "#" space))
                  (zero-or-more (not (any "\n")))
                  eol) nil t)
        (when (file-exists-p (match-string 0))
          (push (match-string 0) files)))
      (nreverse files))))

Feeding our m3u file to our new function conveniently returns a list of found files:

("/absolute/path/to/Music/George Benson/Body Talk/01 Dance.mp3"
 "/absolute/path/to/Music/George Benson/Body Talk/02 When Love Has Grown.mp3"
 "/absolute/path/to/Music/George Benson/Body Talk/03 Plum.mp3"
 "/absolute/path/to/Music/George Benson/Original Album Classics/1-01 So What.mp3"
 "/absolute/path/to/Music/George Benson/Original Album Classics/1-02 The Gentle Rain (From the Film, _The Gentle Rain_).mp3"
 "/absolute/path/to/Music/George Benson/Original Album Classics/1-03 All Clear.mp3"
 "/absolute/path/to/Music/George Benson/The Shape Of Things To Come/01 Footin' It.mp3"
 "/absolute/path/to/Music/George Benson/The Shape Of Things To Come/02 Face It Boy It's Over.mp3"
 "/absolute/path/to/Music/George Benson/The Shape Of Things To Come/03 Shape Of Things To Come.mp3")

Next we need to create a dired buffer from a list of files. This is where I thought things would get trickier, but I was pleasantly surprised.

The dired docstring had the answer:

(defun dired (dirname &optional switches)
  "...

If DIRNAME is a cons, its first element is taken as the directory name
and the rest as an explicit list of files to make directory entries for.
In this case, SWITCHES are applied to each of the files separately, and
therefore switches that control the order of the files in the produced
listing have no effect.

..."
  ...)

With that in mind, this is all it takes:

(let ((default-directory "/absolute/path/to/Music/George Benson"))
  (dired '("*My fancy m3u list*"
           "Body Talk/01 Dance.mp3"
           "Body Talk/02 When Love Has Grown.mp3"
           "Body Talk/03 Plum.mp3"
           "Original Album Classics/1-01 So What.mp3"
           "Original Album Classics/1-02 The Gentle Rain (From the Film, _The Gentle Rain_).mp3"
           "Original Album Classics/1-03 All Clear.mp3"
           "The Shape Of Things To Come/01 Footin' It.mp3"
           "The Shape Of Things To Come/02 Face It Boy It's Over.mp3"
           "The Shape Of Things To Come/03 Shape Of Things To Come.mp3")))

Here's the dired buffer to prove it:

playlist.png

We now have all the pieces. We can wire them up in a ready-player-load-m3u-playlist function.

From the previous snippet, you'd notice all file paths are relative to default-directory. While in the following snippet I use try-completion to find the longest common substring amongst the paths, I wonder if there's a more appropriate built-in function for this? I'd love to hear.

(defun ready-player-load-m3u-playlist ()
  "Load an .m3u playlist."
  (interactive)
  (let* ((m3u-path (read-file-name "find m3u: " nil nil t nil
                                   (lambda (name)
                                     (or (string-match "\\.m3u\\'" name)
                                         (file-directory-p name)))))
         (media-files (if (string-match "\\.m3u\\'" m3u-path)
                          (ready-player--media-at-m3u-file m3u-path)
                        (error "Not a .m3u file")))
         (default-directory (file-name-directory
                             (try-completion "" media-files)))
         (m3u-fname (file-name-nondirectory m3u-path))
         (dired-buffer-name (format "*%s*" m3u-fname))
         (dired-buffer (dired (append (list dired-buffer-name)
                                      (mapcar (lambda (path)
                                                (file-relative-name path default-directory))
                                              media-files)))))
    (ready-player-load-dired-playback-buffer dired-buffer)))

We're good to go now! Invoking M-x ready-player-load-m3u-playlist enables us to load our m3u playlist, automatically opening the first media file, and also navigate each song in the list one by one.

benson.gif

This was a really fun experiment. While dired is often used to manage files within a directory, its magic also extends to dired buffers crafted in more creative ways. find-dired and find-grep-dired are my two favourite built-ins. Are there other ones you like? Do tell.

Not long ago, I added ready-player-load-dired-playback-buffer to ready-player, but ready-player-load-m3u-playlist remains a local experiment (for now anyway). Let's see ;-)

Unrelated - Want your own blog?

Like this blog? Want to start a blog? Run your blog off a single file. Write from the comfort of your favourite text editor and drag and drop to the web. I'm launching a blogging service at lmno.lol. Looking for early adopters. Get in touch.