From:
[email protected]
Date: 2019-05-26
Subject: Playlists, cmus, X3
I don't usually make playlists. I consider myself an album listen-
er. 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 intu-
itive 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 count-
ing 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:
+o cat _catenates_ all the playlist files (which are lists of rel-
ative paths) and sends that to xargs.
+o xargs takes the lines one at a time (-n1) and hands them off to
cp.
+o The --verbose option to xargs tells it to print the commands
it's running to stderr for diagnostic purposes.
+o The -I '{}' tells xargs to replace occurrences of {} with the
file name it got from cat.
+o The single quotes are there to protect them from being inter-
preted by the shell.
+o cp copies the file to the X3.
+o 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 back-
up 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
+o The --progress option tells rsync to output what it's doing.
This is a diagnostic measure.
+o The --update option tells rsync to copy the file only if the
destination file is older or not present.
+o The --files-from option lets us specify a file that contains
the names of the files we want to sync to the destination.
+o That dot sitting there on its own refers to the current direc-
tory (/media/dave/SEAGATE BAC/Music).
+o This basically sets the base path for the file names given in
--files-from.
+o The last argument is the destination folder.
Copying using rsync in this way preserves the source folder struc-
ture at the destination like `cp --parents`, earlier. Using `--up-
date` 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.