I don’t usually make playlists. I consider myself an album listener. I generally buy music on CD or online as a digital download. I store all this on an external drive using a predictable folder structure. Lately, I’ve been buying a lot of singles and EPs that have just a few tracks. I’d like to arrange these songs in a playlist so that I can easily listen to several songs in sequence.

To play music on my desktop machine, I use cmus, a terminal-based player that works really well for me. Well, it leaves a little to be desired when it comes to playlists. It’s easy to load, modify, and write playlists using cmus, but cmus works with only one playlist at a time; you have to provide your own workflow if you want to manage multiple lists.

On the go, I use a Fiio X3ii to play music. It’s a lot like the old iPod. It even has a click-wheel. The software is not as intuitive as the old iPod, though. It has native playlists, but it’s very clunky. I did some research and found that I can drop my own playlists into the X3’s root folder. Once there, I can open them up on the X3 to play all the songs in the playlist.

I want to use cmus to create and manage playlists, and then I will copy them to the X3. There are two problems to tackle, not counting the futzy nature of playlist management in cmus. First, the file paths in the playlists created by cmus include parent folders that won’t be present on the X3. I have to trim those off before I copy the lists to the X3. Second, it would be a hassle to copy the files in the playlists one by one. I need to automate that.

The file paths in the playlist created by cmus look like this:

/media/dave/SEAGATE BAC/Music/SCAR/Out of Perspective EP/SCAR-Clear_as_Day.flac

I manage my playlists in a folder under $HOME. I’ll make a script that will copy the playlists from $HOME/playlists to the Music folder on my external drive. Along the way, I’ll trim the paths so that they are correct once the file is placed on the X3. My script looks like this:

#!/bin/bash
# copy and rebase playlist
for f in ~/playlists/*.pls;
do
  target=$(basename "$f")
  sed -e 's,^/media/dave/SEAGATE BAC/[^/]*/,,' $f > "/media/dave/SEAGATE BAC/Music/${target%.*}.m3u8"
done

The script takes each of my playlist files, hands it off to sed to clean up the path on each line, and writes out the target file. Oh, yeah, the X3 requires playlist files have an ‘m3u8’ extension on them. Other than that, it’s just a list of relative paths.

Now, I can copy this file to my X3 and, assuming the files are also there, the X3 can play the playlist. It’s possible that I’ll add files to a playlist and then forget to copy them to the X3. To avoid that, I’ll create a script to copy each file in each playlist to the X3.

My first swing at this was a brute-force one-liner:

cat *.m3u8 | xargs -n1 --verbose -I '{}' cp --parents '{}' "/media/dave/X3"

Here’s what’s going on:

  • cat catenates all the playlist files (which are lists of relative paths) and sends that to xargs.
  • xargs takes the lines one at a time (-n1) and hands them off to cp.
    • The --verbose option to xargs tells it to print the commands it’s running to stderr for diagnostic purposes.
    • The -I ‘{}’ tells xargs to replace occurrences of {} with the file name it got from cat.
    • The single quotes are there to protect them from being interpreted by the shell.
  • cp copies the file to the X3.
    • The --parents option tells it to keep the parent folders as part of the destination filename.

I call this a brute-force approach because the files are copied to the X3 even if they are already there. I could use test -f to skip the copy if the target file already exists. I could also use rsync, but that may be overkill.

Let’s try overkill first. Rsync is very powerful and flexible. I use it to maintain a duplicate external drive that contains a backup of all my music files. It can easily handle a task like this. First, I’ll make a proof-of-concept.

rsync --progress --update --files-from=dnb.m3u8 . /media/dave/X3
  • The --progress option tells rsync to output what it’s doing. This is a diagnostic measure.
  • The --update option tells rsync to copy the file only if the destination file is older or not present.
  • The --files-from option lets us specify a file that contains the names of the files we want to sync to the destination.
  • That dot sitting there on its own refers to the current directory (/media/dave/SEAGATE BAC/Music).
    • This basically sets the base path for the file names given in --files-from.
  • The last argument is the destination folder.

Copying using rsync in this way preserves the source folder structure at the destination like cp --parents, earlier. Using --update is key because it’s very likely that the files in my playlist are already on the X3 and this prevents the needless transfer of data over a slow connection.

We’re not there yet, though. This command, as written, will sync files from only one playlist at a time. I want to get them all in one shot. I’ll use a feature of bash called process substitution to give rsync the content of all my playlists. It looks like this:

rsync --progress --update --files-from=<(cat *.m3u8) . /media/dave/X3

This works, but it turns out that cmus updates metadata when it plays a file. This makes rsync think the files changed, so it copies them to the X3 again. I just want it to skip those files entirely if they are already there. Another trip to the man page and I find the --ignore-existing option which does exactly what you would expect. So, our command now looks like this:

rsync --progress --ignore-existing --files-from=<(cat *.m3u8) . /media/dave/X3

With a couple modifications, I’ll add it to my script.

#!/bin/bash
#
# rebase playlists, copy to Music folder on external drive
#
musicfolder="/media/dave/SEAGATE BAC/Music"
for f in ~/playlists/*.pls;
do
  target=$(basename "$f")
  sed -e 's,^/media/dave/SEAGATE BAC/[^/]*/,,' $f > "${musicfolder}/${target%.*}.m3u8"
done
#
# copy files and playlists to X3 if it's mounted
#
if [ -d /media/dave/X3 ]; then
  rsync --ignore-existing --progress --files-from=<(cat "${musicfolder}/*.m3u8") "${musicfolder}" /media/dave/X3
  cp  "${musicfolder}/*.m3u8" /media/dave/X3
fi

Now I can manage playlists in cmus and use the script to copy them to the X3 along with all the songs they reference.