Kevin Boone

Command-line hacking: a really simple console music player

I often play music from a command prompt on my Linux systems. Yes, that’s just the way I am. I have a colossal number of albums I’ve ripped from my own CDs, going back about thirty years. I usually organize them on disk in a one-directory-per-album layout but, of course, other arrangements are possible; it’s a matter of preference.

When I play music, I usually play an entire album. So I really just need a way to select a directory from a tree/list, and play everything in it, in alphanumeric order of filename. Not a difficult job, you’d think.

There are, of course, many graphical audio players for Linux, of which Strawberry is currently my favourite. There are also a few console-based audio players, like cmus, and graphical media players might also have a console interface – VLC does. There’s even my own vlc-server, which has console, command-line, and web-based interfaces.

At a pinch, I can just run

$ mplayer /path/to/some/music/*.flac

Sometimes, though, all I want to do is select a directory to play from a scrollable list in the console. I’d like to be able to navigate the folder structure using common keyboard actions like page-up and page-down, find what I want to play, and hit a key to get it played.

Ideally I’d like to be able to do this not only by scrolling a list of folders, but by navigating the metadata tags of my audio files. That’s a bit more problematic than simply browsing a directory tree, but not intractable – more on this later.

The problem with existing console-based music players

Some of the common console audio players have a file browser built in, but I’m not sure how much thought was given to these implementations. VLC has an ncurses (console) interface which you can invoke by running

$ vlc -I ncurses /path/to/files...

Unfortunately, VLC adds the entire set of files in all subdirectories to its playlist, and there’s no way (that I could find) of clearing the playlist and starting again. It’s a shame because VLC (with ncurses) is almost a self-contained console audio player. But not quite – not with a large set of audio files, anyway.

Another possibility is cmus, a dedicated console-based player. cmus, again, is almost there. It has a problem, thought: it’s fiddly to simply play all the files in a selected directory – cmus wants to maintain a library. Unfortunately, the only way it can display the contents of the library is by dividing the collection up into artists. If you like to browse within particular genres, or dates, or anything else except the artist tag, you’re our of luck.

The sad fact is that none of the common console-based music players, so far as I can see, can do the simple job I want.

A simple solution

My solution is to use the nnn file browser, in combination with any player that can play files and directories whose names are given on the command line. The ability to play directories is important, because I want to select and play a whole directory. mplayer, unfortunately, does not play directories – it takes only files or URLs on the command line. To use mplayer we’ll need a helper script, which I’ll describe later.

VLC (with ncurses interface), on the other hand, will take a directory on the command line, and is smart enough to filter out any files that aren’t actually media. So my starting point is a file selector, combined with VLC with its ncurses interface.

Note
I’ll be using a script called nvlc which just runs vlc -I ncurses. This is part of the VLC package on my Linux distribution but, if you don’t have it, it will only take a minute to write.

What about a file selector? There are many console-based file managers; at a pinch you can even use vim as a file manager. Unfortunately, these utilities are really designed for managing files – copying, moving, and deleting them – rather than simply selecting them.

nnn is a console-based file manager which can be used as a simple file/directory picker. On Debian-like systems you can get it using:

$ sudo apt install nnn

When used in its ‘file picker’ mode, with the -p switch, nnn writes the files and directories you select to a file or to standard out. From there, you can feed them into the audio player as command-line arguments.

So my simple console music selector amounts to running this:

$ nvlc "$(nnn -p - /path/to/music)"

Within the file picker, just locate the file or directory using the arrow keys for navigation, and the space bar to select it; then quit the file picker using ‘q’. The player (nvlc in this case) will start up and play the file or directory you selected. When it’s finished, it will stop playing. You could wrap the nvlc command in an endless while loop if you want instead to return to the file picker when playback has finished.

In use, my scheme looks like this:

nnn

Note
Hitting ‘enter’ on a directory expands the directory. If you want to play a directory, rather than a file within it, hit ‘space’ to mark it.

A lighter solution

nnn is a very simple program, which uses minimal resources. VLC however, even with the ncurses interface, is a substantial piece of software. For simple console playback, I’d rather use mplayer, or even mpg123 if I’m playing only MP3 files.

Sadly, mplayer lacks a way to play a whole directory, so we need a helper script that will take a directory on the command line, and expand it into files. It should include only audio files, if possible. Of course, it needs to be able to play individual files, too.

Here is a simple helper script that does this job. I’ve saved it as /usr/bin/vplay.

#!/bin/bash

PLAYER=mplayer
PLAYER_ARGS=(-nogui -vo null)

file_list=()

function add_file_if_valid 
  {
  file=$1
  if [ "${file: -4}" == ".mp3" ] \
      || [ "${file: -5}" == ".flac"  ] \
      || [ "${file: -4}" == ".m4a"  ] \
      || [ "${file: -4}" == ".m4b"  ] \
      || [ "${file: -4}" == ".ogg"  ] ; then
    if [ -f "$file" ] ; then
      file_list+=("$file")
    else
      echo File not found: $file
    fi
  fi
  }

function expand_dir
  {
  dir=$1
  for file in "$dir"/* ; do
    add_file_if_valid "$file"
  done
  }

if [ -z "$1" ] ; then
  echo Usage: $0 {files or directories}
  exit
fi

for file in "$@" ; do
  if [ -d "$file" ] ; then
    expand_dir "$file"
  else
    add_file_if_valid "$file" 
  fi
done

if (( ${#file_list[@]} != 0 )); then
    $PLAYER "${PLAYER_ARGS[@]}" "${file_list[@]}"
else
    echo $0: no valid audio files on the command line.
fi

With this script in place, I can run my console music selector like this:

$ vplay "$(nnn -p - /path/to/music)"

Note
When running mplayer this way, with a set of files from a particular directory, you can use the ‘>’ and ‘<’ keys to skip back and forth between tracks, as well as the usual keys that mplayer provides for navigating within a track.

The problem of metadata

So far, so good: with relatively simple tools, I can select a file or directory from a list, and have it played by the media player. Since I’m quite fussy about how I organize my music files, this suits my needs most of the time.

Sometimes, though, it’s nice to be able to browse using something other than the basic directory structure. I might like to look, for example, at albums with a specific genre tag, or a specific composer.

At this point, it’s probably best to use a console music player that offers this feature; oh, wait – there’s not one. That’s one of the reasons I wrote vlc-server. Again, though, being based on VLC, it’s a bit weighty for a simple job.

There is another way to solve this problem, which kind-of works. It takes advantage of the fact that a file can appear in multiple directories using symbolic links.

So I might have directories like this:


music
  jazz
    albums
      Dave Brubeck - Time Out
        Track1.flac
        Track2.flac
        ...
  artists
    Dave Brubeck
      albums
        Dave Brubeck - Time Out
          Track1.flac
          Track2.flac
          ...

The key point is that the various Track1.flac files are not regular files at all: their symlinks to the same file in some master directory. The music directory, in fact, conists only of directories and links, and can freely be deleted and recreated.

With this ‘index directory’ in place, I can use nnn to browse within the metadata just as I do within an ordinary directory.

But how to create the directory structure, based on audio metadata?

It turns out to be (relatively) simple with a bit of scripting. We need to enumerate every file in the master music directory, and get its metadata. There are various ways to do this; a simple approach is to use ffprobe, which is part of the ffmpeg package.

$ ffprobe -loglevel quiet -show_entries format_tags "$file"

This produces output of the form:

[FORMAT]
TAG:TITLE=1 Blue Rondo A La Turk
TAG:ARTIST=Dave Brubeck
TAG:ALBUM=Dave Brubeck - Time Out (FLAC)
TAG:track=01
TAG:GENRE=music - jazz
TAG:COMPOSER=Brubeck, Dave
[/FORMAT]

It’s easy enough to parse out the specific tag values using a mixture of grep and cut, although we do have to be a bit careful because, for some reason, the tag names don’t appear with consistent letter case.

With the tag values extracted, we can create the files and symbolic links in our ‘index’ directory with combinations of mkdir and ln -sf.

Because I might delete audio files from my collection, as well as adding new ones, the final job the script has to do is to purge all the links and directories that no longer point to valid targets.

I’m not going to present my script that does this job, for two reasons.

First, it’s an ugly hack.

Second, in practice I don’t use ffprobe for this, because it’s too slow. ffprobe is way too heavyweight for extracting tags, even though it supports a huge range of file types. Instead, I use a utility I wrote myself in C. My utility is nowhere near as comprehensive as ffprobe, but it handles all the types of audio file I use.

I could be persuaded to share my index-creation script, if anybody was interested; but implementing something like this is a good exercise in script hacking.

Final thoughts

It’s a shame that, after all this time, there aren’t any simple, lightweight, console-based music players for Linux that really work. Some come close, but none suits my needs which, frankly, are not demanding.

My solution, using a command-line audio player and nnn as a file picker is hardly elegant, but it is at least lightweight and, in fact, works quite well for my simple purposes.