2023-05-06 Juggling playlists in EMMS

It’s been several times now that I described my productivity system here. While I like it a lot, it still has room for improvement. Some time ago I mentioned it to a colleague and shared one frustration I have with it. When I fail to do even one thing on my carefully prepared plan for a day (which happens once in a while, of course), sometimes I lose motivation and skip more subsequent items. This leads to spending less time on things I want to do and more time on procrastination that day. It happens seldom enough not to be a real issue (and it doesn’t mean I fail to do anything – just less than usual or hoped for), but it is a bit frustrating. He suggested a pretty clever (and obvious in hindsight) way to deal with this. His idea is to have a separate music playlist „for work” and train my brain to associate that particular music with focused work. That way I would have another nudge to do particular kind of stuff.

This means I would need two things. One is selecting some set of pieces I like to listen to and putting it aside when I’m at home. It has to be large enough to be possible to listen to for several hours without much repetition (easy – I have and listen to a lot of music), and I’d have to commit to not listening to it at home or during commuting (a bit harder – if I like some music, I might want to listen to it a lot of the time – but doable). Two is that I’d like to write some Elisp to automate switching between playlists.

Switching to another playlist is very easy. Or so it seems:

(defun emms-playlist-load-and-shuffle (file) ; this code doesn't work!
  "Load playlist from FILE, shuffle it and select the first track."
  (emms-playlist-current-clear)
  (emms-add-playlist file)
  (emms-shuffle)
  (emms-playlist-current-select-first))

The issue with the above code is a very subtle one. When it is called and some track from the current playlist is being played, the playlist is cleared first and a new one is loaded. However, in such a case the “selected track” (the one that is being played) disappears. Then, emms-shuffle errors out, since it tries to do something with the selected track, and there is none. The solution I found is not very elegant, but works pretty well.

(defun emms-playlist-current-clear-but-current-track ()
  "Clear the current playlist, leaving the currently played track."
  (with-current-buffer emms-playlist-buffer
    (let ((inhibit-read-only t))
      (widen)
      (goto-char emms-playlist-selected-marker)
      (delete-region (point-min)
                     (line-beginning-position))
      (delete-region (min (point-max)
                          (1+ (line-end-position)))
                     (point-max)))))

(defun emms-playlist-load-and-shuffle (file)
  "Load playlist from FILE, shuffle it and select the first track."
  (emms-playlist-current-clear-but-current-track)
  (emms-add-playlist file)
  (emms-shuffle))

The emms-playlist-current-clear-but-current-track is mostly copied from emms-playlist-current-clear and emms-playlist-clear. It removes all tracks from the playlist with the exception of the current one. Then it adds the new playlist and shuffles it. Shuffling in EMMS is implemented so that the currently selected track becomes the first one - in our case, it was the first one anyway. Hence, the currently played track finishes and the next one is then taken from the “new” playlist. This is actually quite cool – instead of abruptly stopping playing, my code lets the current track finish and only then the new playlist takes over. The trickiest part was (goto-char emms-playlist-selected-marker) – it was not at all abvious to me that EMMS does not move the point to the current track. When I switched to the playlist buffer, selected a track manually and called emms-playlist-current-clear-but-current-track, everything was fine, since the point was on the current track. But then I started using my code, letting many tracks to be played before making a switch, and then suddenly things started breaking, because the point was no longer on the track being played. Happily, once I knew what the issue was, fixing it became easy – it was enough to look at the source of emms-playlist-current-selected-track and then emms-playlist-selected-track. As usual, the ability of Emacs to easily show me the source of any function came in very handy.

I could add some code removing the first track to emms-player-finished-hook, but I would have to make sure it removes itself from that hook then – neither elegant nor really needed. Each one of my playlists is 10 hours long at least, so it will never have a chance to get to the end before the end of day – or even the weekend, as I don’t sit too much at my computer on weekends – so it will never get back to this first track, even assuming I’d put it on “repeat”.

That – along with two playlist files, say work.playlist and home.playlist – is enough to switch my playlists manually.

So, to make it fancy and automatic I need Emacs to know if I’m at the office or at home (while I do work from home sometimes, it’s rare enough I don’t need to take this into account). One way to know that is to check the SSID of the wifi network I’m on. This AskUbuntu answer tells me that iwgetid -r is the way to get it from command line without sudo.

(require 's)
(defun places--get-ssid ()
  "Get the SSID of the active network."
  (s-trim-right (shell-command-to-string "iwgetid -r")))

(I’m going to use a places- prefix for all stuff related to changing Emacs’ behavior depending on where I am.) This is probably not the optimal solution – for example, it won’t work when I don’t use wifi – but it’s good enough for me. Now I need a mapping from SSIDs to places:

(defcustom places-ssid-to-place nil
  "An alist mapping SSIDs (as strings) to places.")
(setq places-ssid-to-place '(("office-wifi" . office)
                             ("home-wifi" . home)))

Notice that I define this in two steps – defcustom defines the special variable places-ssid-to-place and gives it a docstring, and setq sets the value I need. This is because it doesn’t make any sense to have a “default” value – every person using this is going to have a different value (assuming, of course, that this code will be used by anyone except me – perhaps not very likely, but still possible). I could do without defcustom, of course, but then I’d lose the ability to set a docstring (and places-ssid-to-place would be lexically bound, which is not what I want, though I’m not sure if it matters much).

Now I’d like to be able to run some code when I get to work or home. This is a bit tricky. One way would be to configure systemd to run some script whenever an internet connection is established, and call emacsclient --eval with some Elisp to run. Since I’m only interested in a proof-of-concept implementation for now (and I’m not sure I even want to touch systemd), I’ll go with a simpler solution – a periodic check (using a timer) for the SSID.

The way I’m going to do this is by creating a mapping from places to lists of functions to be called when I “enter” a given place.

(defcustom places-functions nil
  "An alist of lists of functions to be run when at a new place.")
(setq places-functions
      '((office . ())
        (home . ())))

This way I can do things beyond switching playlists if I ever need that. For example, I can make Emacs launch some software I use at work automatically – something I’m doing manually right now. Also, since I want the functions in places-functions to be run without arguments – and I don’t want to have lambdas there – I will define two functions for switching to a particular playlist.

(defun emms-load-and-shuffle-office-playlist ()
  "Load and shuffle `office.playlist'."
  (emms-playlist-load-and-shuffle "/path/to/music/office.playlist"))

(defun emms-load-and-shuffle-home-playlist ()
  "Load and shuffle `home.playlist'."
  (emms-playlist-load-and-shuffle "/path/to/music/home.playlist"))

Now the switching code itself.

(defvar places--current-place nil
  "The place I'm in right now.")

(defun places--check-for-new-place ()
  "Check if I'm in a new place.
If so, run a suitable set of functions from `places-functons'."
  (let ((old-place places--current-place)
        (new-place (alist-get (places--get-ssid)
                              places-ssid-to-place
                              nil nil #'string=)))
    (when (and new-place
               (not (eq old-place new-place)))
      (setq places--current-place new-place)
      (message "Went from %s to %s at %s"
               old-place
               new-place
               (format-time-string "%F %T"))
      (mapc #'funcall
            (alist-get new-place
                       places-functions
                       nil nil #'string=)))))

(defcustom places-interval 30
  "The interval (in seconds) between checks for a new place.")

(run-at-time 0 places-interval #'places--check-for-new-place)

(setq places-functions
      '((office . (emms-load-and-shuffle-office-playlist))
        (home . (emms-load-and-shuffle-home-playlist))))

Notice how I do not “change places” when new-place is nil. If I lose connectivity (which happens from time to time), the SSID reported is an empty string, so new-place becomes nil. Despite it being different than before, a change of place is not triggered. The same thing happens in a new place unknown to the system – my code plays it safe and assumes that if I was at home and then moved to somewhere it doesn’t know, the place is not changed from 'home to anything.

So, I’ve been now using this code for a bit more than a week now, and I have to say that it works pretty fine. I am a bit skeptical whether it will really help me with my focus at work. Even if it doesn’t, though, it was a pretty cool little project, and I learned something – and had some fun – along the way. I may even be tempted to add this (or something similar) to my Emacs Lisp book (which I again recommend to check out if you want to learn to code stuff like this)!

CategoryEnglish, CategoryBlog, CategoryEmacs