Where parallels cross

Interesting bits of life

Emms + Org Roam: YouTube playlists with titles!

Too long; didn't read

I extend Emms to parse EXTM3U playlists of youtube links so I can listen to songs with MPV and read their titles!

The problem

I like music. And lucky me YouTube is a treasure chest of songs! I already wrote how I made VLC become my YouTube music player. Well somehow that stopped working: YouTube API changes often enough to break VLC plugins. Also what a pity I wasn't using GNU Emms! That is a great mode for playing a song with a few keybindings.

I got also used to capture music I like as Org Roam notes: I use Org-Feed with a Reddit RSS that sends me some random music that redditors like, and I extract Org Roam notes for those songs I like.

Now I have musical notes that can make a playlist: how to get them played?

And there is a solution

Emms + Mpv + youtube-dl is the answer :) At least part of it. Let me explain.

Youtube-dl is a software that lets you download YouTube videos. Since that is its core feature, it should not fail when the API changes.

Mpv is a free media player and leverages youtube-dl to play YouTube links: it downloads them in a temporary directory and plays them while youtube-dl is downloading (i.e., streaming).

Emms supports Mpv by default. So after you install these dependencies you are already at a good point.

The next step is to produce a list of your links and feed them to Mpv. Mpv accepts playlist files in the M3U format. This is basically a list of files or urls in a text file.

With that you can run mpv your-playlist.m3u and it will start playing. Before we move on the next issue, let's plugin Emms. If you run emms-play-m3u-playlist giving that file, the music will play. BUT also the video! In my case that wasn't necessary: I want to listen to songs and not watch their videos. You can fix that by setting some Emms variables. Here my Emms config code:

(emms-all)
(emms-default-players)
(setq-default
   emms-source-file-default-directory "~/Music/"

   emms-source-playlist-default-format 'm3u
   emms-playlist-mode-center-when-go t
   emms-playlist-default-major-mode 'emms-playlist-mode
   emms-show-format "NP: %s"

   emms-player-list '(emms-player-mpv)
   emms-player-mpv-environment '("PULSE_PROP_media.role=music")
   emms-player-mpv-parameters '("--quiet" "--really-quiet" "--no-video" "--no-audio-display" "--force-window=no" "--vo=null"))

The relevant bit is '("--quiet" "--really-quiet" "--no-video" "--no-audio-display" "--force-window=no" "--vo=null"), which is telling to avoid displaying Mpv window AND to skip the video.

This is probably something you don't want to set if you also watch videos with Mpv from Emms, but that is not my case.

Now to the fun part: how do you create the playlist? Well Org Roam API to the rescue: since I backlink songs to a common note, I just need to gather these notes with some Elisp and print their links in a M3U file.

When you open a playlist of YouTube urls (the M3U file), you will discover that running emms-playlist-mode-go will show just a list of links. If you want to know the title of this song, or if you want to listen to a particular song, you have to memorize YouTube links. I am not good at that.

So with a bit of browsing, I discovered that there is a fancier way to define a playlist: an EXTM3U format (I guess EXT is for extended). This allows to provide the title and length of the song along to its file or link.

Naturally... Emms doesn't support the extended format. At least a code comment tells that is not implemented yet. Well I need it, so I will implement it. (If an Emms developer reads this: please feel free to "get inspired" by my code ;)

This also gives me a chance to show-off how nicely the Emms developers made it for contributors to add their own new playlist parser. The idea is that you need 3 things:

  1. a way to find out if the file you open is indeed a playlist,
  2. a way to extract the songs and make emms "tracks" out of them
  3. a way to convert emms tracks back into the playlist file format

In the following I cover 1 and 2, and I ignore 3 (because I keep the playlist links as Org Roam notes, which is more powerful than a text file).

How do we recognize an EXTM3U file? Simple! It has to start with a header: #EXTM3U.

(defun emms-source-playlist-extm3u-p ()
    "Return non-nil if the current buffer contains an extm3u playlist."
    (save-excursion
      (goto-char (point-min))
      (s-contains? "#EXTM3U" (buffer-string))))

That was easy! Next, parse the playlist with names. Let's peek into how Emms parses m3u playlists first:

(defun emms-source-playlist-parse-m3u (playlist-file)
  "Parse the m3u playlist in the current buffer.
Files will be relative to the directory of PLAYLIST-FILE, unless
they have absolute paths."
  (let ((dir (file-name-directory playlist-file)))
    (mapcar (lambda (file)
              (if (string-match "\\`\\(http[s]?\\|mms\\)://" file)
                  (emms-track 'url file)
                (emms-track 'file (expand-file-name file dir))))
            (emms-source-playlist-m3u-files))))

This gathers links in the EXTM3U file with emms-source-playlist-m3u-files and then for each, if it is a link creates a URL emms-track, otherwise a filepath one.

Cool, let's start from gathering urls and titles for our extm3u counterpart:

(defun emms-source-playlist-extm3u-files-names ()
    "Extract a list of filenames from the given extm3u playlist.

Empty lines and lines starting with '#' are ignored."
    (--keep
     (and (not (s-starts-with-p "#" it))
          (--> it
               (s-split "\n" it t)
               (list :name (nth 1 (s-split "," (nth 0 it) t)) :file (nth 1 it))))
     (s-split "#EXTINF:" (buffer-string))))

I am using dash.el here because I like that style of coding. In short it

  1. splits the file contents on "#EXTINF:"

    that header identifies the start of a song definition, so splitting will create N+1 entries each containing the song info

    We ignore the first that contains the main header of the file (i.e., #EXTM3U)

  2. ignores the entry's header and extract name and file from each entry I found before as a plist (:name :file)

Now we can feed that in our main parsing function:

(defun emms-source-playlist-parse-extm3u (playlist-file)
  "Parse the m3u playlist in the current buffer.
Files will be relative to the directory of PLAYLIST-FILE, unless
they have absolute paths."
  (let ((dir (file-name-directory playlist-file)))
    (mapcar (lambda (name-file)
              (let* ((file (plist-get name-file :file))
                     (track (if (string-match "\\`\\(http[s]?\\|mms\\)://" file)
                                (emms-track 'url file)
                              (emms-track 'file (expand-file-name file dir))))
                     (_ (emms-track-set track 'info-title (plist-get name-file :name))))
                track))
            (emms-source-playlist-extm3u-files-names))))

The only change here from the Emms' m3u function is that we set the title for the track with (emms-track-set track 'info-title (plist-get name-file :name)).

Now if you load these functions and open an EXTM3U playlist, Emms will open it AND display the title as well. I typically store music notes in a format "AUTHOR -- TITLE", so I didn't bother to set the author information for the track (yet). Hopefully Emms provides a track property for that as well :)

That's it! Now I can listen to music with Emms, know what I am playing AND finally be able to choose the song I like. All of this keeping my hard-drive empty.

Conclusion

If you didn't think you could listen/manage music from YouTube within Emacs think again :D Also Emms developers are awesome!

Happy listening!

Comments