From: dbucklin@sdf.org
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.